protocol-hpack 1.4.3 → 1.5.1
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/protocol/hpack/compressor.rb +9 -11
- data/lib/protocol/hpack/context.rb +47 -19
- data/lib/protocol/hpack/decompressor.rb +5 -3
- data/lib/protocol/hpack/huffman/generator.rb +186 -0
- data/lib/protocol/hpack/huffman/machine.rb +254 -254
- data/lib/protocol/hpack/huffman.rb +14 -11
- data/lib/protocol/hpack/version.rb +1 -1
- data/lib/protocol/hpack.rb +2 -2
- data/license.md +2 -0
- data/readme.md +8 -8
- data.tar.gz.sig +0 -0
- metadata +8 -6
- metadata.gz.sig +0 -0
- data/tasks/huffman.rake +0 -10
- data/tasks/huffman.rb +0 -173
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: ad81d71708c7c92bb94e9616655872045a31052aa62c3543487064ddea8c99ad
         | 
| 4 | 
            +
              data.tar.gz: 1945361f8e8576ef3db44de1e0cf44554795699d910c40b8659232a499ded6b8
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: c0537372d9278691fa9621622b84f32facde4f9efdfbfd0c36eb7573bba4b19ec05bc91f5191a7463405cbed952c4c6ac614531c4c0fc0b26707196db8d800b3
         | 
| 7 | 
            +
              data.tar.gz: fc53196d54e9b02a56df5c1294e819f5a4002c49201811c12d2666725a0325e27df75603afc4b04a5fc5c1845237937097bf51352ea4d5f1c3741d1b73fa244a
         | 
    
        checksums.yaml.gz.sig
    CHANGED
    
    | Binary file | 
| @@ -11,9 +11,10 @@ | |
| 11 11 | 
             
            # Copyright, 2018, by Kenichi Nakamura.
         | 
| 12 12 | 
             
            # Copyright, 2019, by Jingyi Chen.
         | 
| 13 13 | 
             
            # Copyright, 2020, by Justin Mazzocchi.
         | 
| 14 | 
            +
            # Copyright, 2024, by Nathan Froyd.
         | 
| 14 15 |  | 
| 15 | 
            -
            require_relative  | 
| 16 | 
            -
            require_relative  | 
| 16 | 
            +
            require_relative "context"
         | 
| 17 | 
            +
            require_relative "huffman"
         | 
| 17 18 |  | 
| 18 19 | 
             
            module Protocol
         | 
| 19 20 | 
             
            	module HPACK
         | 
| @@ -74,22 +75,19 @@ module Protocol | |
| 74 75 | 
             
            			# @param bits [Integer] number of available bits
         | 
| 75 76 | 
             
            			# @return [String] binary string
         | 
| 76 77 | 
             
            			def write_integer(value, bits)
         | 
| 77 | 
            -
            				limit =  | 
| 78 | 
            +
            				limit = (1 << bits) - 1
         | 
| 78 79 |  | 
| 79 | 
            -
            				return  | 
| 80 | 
            +
            				return @buffer << value if value < limit
         | 
| 80 81 |  | 
| 81 | 
            -
            				 | 
| 82 | 
            -
            				bytes.push(limit) unless bits.zero?
         | 
| 82 | 
            +
            				@buffer << limit unless bits.zero?
         | 
| 83 83 |  | 
| 84 84 | 
             
            				value -= limit
         | 
| 85 85 | 
             
            				while value >= 128
         | 
| 86 | 
            -
            					 | 
| 86 | 
            +
            					@buffer << ((value & 0x7f) + 128)
         | 
| 87 87 | 
             
            					value /= 128
         | 
| 88 88 | 
             
            				end
         | 
| 89 89 |  | 
| 90 | 
            -
            				 | 
| 91 | 
            -
            				
         | 
| 92 | 
            -
            				write_bytes(bytes.pack('C*'))
         | 
| 90 | 
            +
            				@buffer << value
         | 
| 93 91 | 
             
            			end
         | 
| 94 92 |  | 
| 95 93 | 
             
            			def huffman
         | 
| @@ -118,7 +116,7 @@ module Protocol | |
| 118 116 | 
             
            			# @return [String] binary string
         | 
| 119 117 | 
             
            			def write_string(string, huffman = self.huffman)
         | 
| 120 118 | 
             
            				if huffman != :never
         | 
| 121 | 
            -
            					encoded = Huffman. | 
| 119 | 
            +
            					encoded = Huffman.encode(string)
         | 
| 122 120 |  | 
| 123 121 | 
             
            					if huffman == :shorter and encoded.bytesize >= string.bytesize
         | 
| 124 122 | 
             
            						encoded = nil
         | 
| @@ -2,8 +2,10 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            # Released under the MIT License.
         | 
| 4 4 | 
             
            # Copyright, 2018-2024, by Samuel Williams.
         | 
| 5 | 
            +
            # Copyright, 2024, by Maruth Goyal.
         | 
| 6 | 
            +
            # Copyright, 2024, by Nathan Froyd.
         | 
| 5 7 |  | 
| 6 | 
            -
            require_relative  | 
| 8 | 
            +
            require_relative "huffman"
         | 
| 7 9 |  | 
| 8 10 | 
             
            module Protocol
         | 
| 9 11 | 
             
            	# Implementation of header compression for HTTP 2.0 (HPACK) format adapted
         | 
| @@ -94,7 +96,20 @@ module Protocol | |
| 94 96 | 
             
            				["via", ""],
         | 
| 95 97 | 
             
            				["www-authenticate", ""],
         | 
| 96 98 | 
             
            			].each(&:freeze).freeze
         | 
| 97 | 
            -
             | 
| 99 | 
            +
             | 
| 100 | 
            +
            			STATIC_EXACT_LOOKUP = {}
         | 
| 101 | 
            +
            			STATIC_NAME_LOOKUP = {}
         | 
| 102 | 
            +
             | 
| 103 | 
            +
            			STATIC_TABLE.each_with_index do |(name, value), i|
         | 
| 104 | 
            +
            				exact_header_values = (STATIC_EXACT_LOOKUP[name] ||= [])
         | 
| 105 | 
            +
            				exact_header_values << [value, i]
         | 
| 106 | 
            +
            				STATIC_NAME_LOOKUP[name] = i if STATIC_NAME_LOOKUP[name].nil?
         | 
| 107 | 
            +
            			end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
            			STATIC_EXACT_LOOKUP.each {|k, v| v.freeze}
         | 
| 110 | 
            +
            			STATIC_EXACT_LOOKUP.freeze
         | 
| 111 | 
            +
            			STATIC_NAME_LOOKUP.freeze
         | 
| 112 | 
            +
             | 
| 98 113 | 
             
            			# Initializes compression context with appropriate client/server defaults and maximum size of the dynamic table.
         | 
| 99 114 | 
             
            			#
         | 
| 100 115 | 
             
            			# @param table [Array] Table of header key-value pairs.
         | 
| @@ -233,29 +248,42 @@ module Protocol | |
| 233 248 | 
             
            			#  :static  Use static table only.
         | 
| 234 249 | 
             
            			#  :all     Use all of them.
         | 
| 235 250 | 
             
            			#
         | 
| 236 | 
            -
            			# @param  | 
| 251 | 
            +
            			# @param name [String]
         | 
| 252 | 
            +
            			# @param value [String]
         | 
| 237 253 | 
             
            			# @return [Hash] command
         | 
| 238 | 
            -
            			def add_command( | 
| 254 | 
            +
            			def add_command(name, value)
         | 
| 239 255 | 
             
            				exact = nil
         | 
| 240 256 | 
             
            				name_only = nil
         | 
| 241 257 |  | 
| 242 | 
            -
            				if  | 
| 243 | 
            -
            					 | 
| 244 | 
            -
            						 | 
| 245 | 
            -
            							 | 
| 246 | 
            -
             | 
| 247 | 
            -
             | 
| 248 | 
            -
            							 | 
| 258 | 
            +
            				if @index == :all || @index == :static
         | 
| 259 | 
            +
            					if (values_and_indices = STATIC_EXACT_LOOKUP[name])
         | 
| 260 | 
            +
            						values_and_indices.each do |known_value, index|
         | 
| 261 | 
            +
            							if value == known_value
         | 
| 262 | 
            +
            								exact = index
         | 
| 263 | 
            +
            								break
         | 
| 264 | 
            +
            							end
         | 
| 249 265 | 
             
            						end
         | 
| 266 | 
            +
            						
         | 
| 267 | 
            +
            						needs_name_lookup = exact.nil?
         | 
| 268 | 
            +
            					else
         | 
| 269 | 
            +
            						needs_name_lookup = true
         | 
| 270 | 
            +
            					end
         | 
| 271 | 
            +
             | 
| 272 | 
            +
            					if needs_name_lookup && (static_value = STATIC_NAME_LOOKUP[name])
         | 
| 273 | 
            +
            						name_only = static_value
         | 
| 250 274 | 
             
            					end
         | 
| 251 275 | 
             
            				end
         | 
| 252 | 
            -
             | 
| 276 | 
            +
             | 
| 277 | 
            +
            				if @index == :all && !exact
         | 
| 253 278 | 
             
            					@table.each_index do |i|
         | 
| 254 | 
            -
            						 | 
| 255 | 
            -
             | 
| 256 | 
            -
            							 | 
| 257 | 
            -
             | 
| 258 | 
            -
             | 
| 279 | 
            +
            						entry = @table[i]
         | 
| 280 | 
            +
            						if entry.first == name
         | 
| 281 | 
            +
            							if entry.last == value
         | 
| 282 | 
            +
            								exact ||= i + STATIC_TABLE.size
         | 
| 283 | 
            +
            								break
         | 
| 284 | 
            +
            							else
         | 
| 285 | 
            +
            								name_only ||= i + STATIC_TABLE.size
         | 
| 286 | 
            +
            							end
         | 
| 259 287 | 
             
            						end
         | 
| 260 288 | 
             
            					end
         | 
| 261 289 | 
             
            				end
         | 
| @@ -263,9 +291,9 @@ module Protocol | |
| 263 291 | 
             
            				if exact
         | 
| 264 292 | 
             
            					{name: exact, type: :indexed}
         | 
| 265 293 | 
             
            				elsif name_only
         | 
| 266 | 
            -
            					{name: name_only, value:  | 
| 294 | 
            +
            					{name: name_only, value: value, type: :incremental}
         | 
| 267 295 | 
             
            				else
         | 
| 268 | 
            -
            					{name:  | 
| 296 | 
            +
            					{name: name, value: value, type: :incremental}
         | 
| 269 297 | 
             
            				end
         | 
| 270 298 | 
             
            			end
         | 
| 271 299 |  | 
| @@ -2,9 +2,11 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            # Released under the MIT License.
         | 
| 4 4 | 
             
            # Copyright, 2018-2024, by Samuel Williams.
         | 
| 5 | 
            +
            # Copyright, 2024, by Maruth Goyal.
         | 
| 6 | 
            +
            # Copyright, 2024, by Nathan Froyd.
         | 
| 5 7 |  | 
| 6 | 
            -
            require_relative  | 
| 7 | 
            -
            require_relative  | 
| 8 | 
            +
            require_relative "context"
         | 
| 9 | 
            +
            require_relative "huffman"
         | 
| 8 10 |  | 
| 9 11 | 
             
            module Protocol
         | 
| 10 12 | 
             
            	module HPACK
         | 
| @@ -88,7 +90,7 @@ module Protocol | |
| 88 90 |  | 
| 89 91 | 
             
            				raise CompressionError, "Invalid string length, got #{string.bytesize}, expecting #{length}!" unless string.bytesize == length
         | 
| 90 92 |  | 
| 91 | 
            -
            				string = Huffman. | 
| 93 | 
            +
            				string = Huffman.decode(string) if huffman
         | 
| 92 94 |  | 
| 93 95 | 
             
            				return string.force_encoding(Encoding::UTF_8)
         | 
| 94 96 | 
             
            			end
         | 
| @@ -0,0 +1,186 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # Released under the MIT License.
         | 
| 4 | 
            +
            # Copyright, 2014, by Kaoru Maeda.
         | 
| 5 | 
            +
            # Copyright, 2015, by Tamir Duberstein.
         | 
| 6 | 
            +
            # Copyright, 2015, by Ilya Grigorik.
         | 
| 7 | 
            +
            # Copyright, 2016, by George Ulmer.
         | 
| 8 | 
            +
            # Copyright, 2018-2024, by Samuel Williams.
         | 
| 9 | 
            +
            # Copyright, 2024, by Nathan Froyd.
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            require_relative "../huffman"
         | 
| 12 | 
            +
            require "set"
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            module Protocol
         | 
| 15 | 
            +
            	module HPACK
         | 
| 16 | 
            +
            		class Huffman
         | 
| 17 | 
            +
            			module Generator
         | 
| 18 | 
            +
            				EOS = 256
         | 
| 19 | 
            +
            				
         | 
| 20 | 
            +
            				class Node
         | 
| 21 | 
            +
            					attr_accessor :next, :emit, :final, :depth
         | 
| 22 | 
            +
            					attr_accessor :transitions
         | 
| 23 | 
            +
            					attr_accessor :id
         | 
| 24 | 
            +
            					
         | 
| 25 | 
            +
            					@@id = 0
         | 
| 26 | 
            +
            					
         | 
| 27 | 
            +
            					def initialize(depth)
         | 
| 28 | 
            +
            						@next = [nil, nil]
         | 
| 29 | 
            +
            						@id = @@id
         | 
| 30 | 
            +
            						@@id += 1
         | 
| 31 | 
            +
            						@final = false
         | 
| 32 | 
            +
            						@depth = depth
         | 
| 33 | 
            +
            					end
         | 
| 34 | 
            +
            					
         | 
| 35 | 
            +
            					def add(code, len, chr)
         | 
| 36 | 
            +
            						self.final = true if chr == EOS && @depth <= 7
         | 
| 37 | 
            +
            						if len.zero?
         | 
| 38 | 
            +
            							@emit = chr
         | 
| 39 | 
            +
            						else
         | 
| 40 | 
            +
            							bit = (code & (1 << (len - 1))).zero? ? 0 : 1
         | 
| 41 | 
            +
            							node = @next[bit] ||= Node.new(@depth + 1)
         | 
| 42 | 
            +
            							node.add(code, len - 1, chr)
         | 
| 43 | 
            +
            						end
         | 
| 44 | 
            +
            					end
         | 
| 45 | 
            +
            					
         | 
| 46 | 
            +
            					class Transition
         | 
| 47 | 
            +
            						attr_accessor :emit, :node
         | 
| 48 | 
            +
            						def initialize(emit, node)
         | 
| 49 | 
            +
            							@emit = emit
         | 
| 50 | 
            +
            							@node = node
         | 
| 51 | 
            +
            						end
         | 
| 52 | 
            +
            					end
         | 
| 53 | 
            +
            					
         | 
| 54 | 
            +
            					def self.generate_tree
         | 
| 55 | 
            +
            						@root = new(0)
         | 
| 56 | 
            +
            						Protocol::HPACK::Huffman::CODES.each_with_index do |c, chr|
         | 
| 57 | 
            +
            							code, len = c
         | 
| 58 | 
            +
            							@root.add(code, len, chr)
         | 
| 59 | 
            +
            						end
         | 
| 60 | 
            +
            						puts "#{@@id} nodes"
         | 
| 61 | 
            +
            						@root
         | 
| 62 | 
            +
            					end
         | 
| 63 | 
            +
            					
         | 
| 64 | 
            +
            					def self.generate_machine
         | 
| 65 | 
            +
            						generate_tree
         | 
| 66 | 
            +
            						
         | 
| 67 | 
            +
            						# Using un-ordered sets (potentially) produces non-deterministic results:
         | 
| 68 | 
            +
            						togo = Set[@root]
         | 
| 69 | 
            +
            						@states = Set[@root]
         | 
| 70 | 
            +
            						
         | 
| 71 | 
            +
            						until togo.empty?
         | 
| 72 | 
            +
            							node = togo.first
         | 
| 73 | 
            +
            							togo.delete(node)
         | 
| 74 | 
            +
            							
         | 
| 75 | 
            +
            							next if node.transitions
         | 
| 76 | 
            +
            							node.transitions = Array[1 << BITS_AT_ONCE]
         | 
| 77 | 
            +
            							
         | 
| 78 | 
            +
            							(1 << BITS_AT_ONCE).times do |input|
         | 
| 79 | 
            +
            								n = node
         | 
| 80 | 
            +
            								emit = +""
         | 
| 81 | 
            +
            								(BITS_AT_ONCE - 1).downto(0) do |i|
         | 
| 82 | 
            +
            									bit = (input & (1 << i)).zero? ? 0 : 1
         | 
| 83 | 
            +
            									n = n.next[bit]
         | 
| 84 | 
            +
            									next unless n.emit
         | 
| 85 | 
            +
            									if n.emit == EOS
         | 
| 86 | 
            +
            										emit = EOS # cause error on decoding
         | 
| 87 | 
            +
            									else
         | 
| 88 | 
            +
            										emit << n.emit.chr(Encoding::BINARY) unless emit == EOS
         | 
| 89 | 
            +
            									end
         | 
| 90 | 
            +
            									n = @root
         | 
| 91 | 
            +
            								end
         | 
| 92 | 
            +
            								node.transitions[input] = Transition.new(emit, n)
         | 
| 93 | 
            +
            								togo << n
         | 
| 94 | 
            +
            								@states << n
         | 
| 95 | 
            +
            							end
         | 
| 96 | 
            +
            						end
         | 
| 97 | 
            +
            						puts "#{@states.size} states"
         | 
| 98 | 
            +
            						@root
         | 
| 99 | 
            +
            					end
         | 
| 100 | 
            +
            					
         | 
| 101 | 
            +
            					MACHINE_PATH = File.expand_path("machine.rb", __dir__)
         | 
| 102 | 
            +
            					
         | 
| 103 | 
            +
            					def self.generate_state_table(output_path = MACHINE_PATH)
         | 
| 104 | 
            +
            						generate_machine
         | 
| 105 | 
            +
            						state_id = {}
         | 
| 106 | 
            +
            						id_state = {}
         | 
| 107 | 
            +
            						state_id[@root] = 0
         | 
| 108 | 
            +
            						id_state[0] = @root
         | 
| 109 | 
            +
            						max_final = 0
         | 
| 110 | 
            +
            						id = 1
         | 
| 111 | 
            +
            						(@states - [@root]).sort_by {|s| s.final ? 0 : 1}.each do |s|
         | 
| 112 | 
            +
            							state_id[s] = id
         | 
| 113 | 
            +
            							id_state[id] = s
         | 
| 114 | 
            +
            							max_final = id if s.final
         | 
| 115 | 
            +
            							id += 1
         | 
| 116 | 
            +
            						end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
            						File.open(output_path, "w") do |file|
         | 
| 119 | 
            +
            							file.print <<~HEADER
         | 
| 120 | 
            +
            								# frozen_string_literal: true
         | 
| 121 | 
            +
             | 
| 122 | 
            +
            								# Released under the MIT License.
         | 
| 123 | 
            +
            								# Copyright, 2018-2024, by Samuel Williams.
         | 
| 124 | 
            +
             | 
| 125 | 
            +
            								# Machine generated Huffman decoder state machine.
         | 
| 126 | 
            +
            								# DO NOT EDIT THIS FILE.
         | 
| 127 | 
            +
             | 
| 128 | 
            +
            								module Protocol
         | 
| 129 | 
            +
            									module HPACK
         | 
| 130 | 
            +
            										class Huffman
         | 
| 131 | 
            +
            											# :nodoc:
         | 
| 132 | 
            +
            											MAX_FINAL_STATE = #{max_final}
         | 
| 133 | 
            +
            											MACHINE = [
         | 
| 134 | 
            +
            							HEADER
         | 
| 135 | 
            +
            							
         | 
| 136 | 
            +
            							id.times do |i|
         | 
| 137 | 
            +
            								n = id_state[i]
         | 
| 138 | 
            +
            								file.print "\t\t\t\t["
         | 
| 139 | 
            +
            								string = (1 << BITS_AT_ONCE).times.map do |t|
         | 
| 140 | 
            +
            									transition = n.transitions.fetch(t)
         | 
| 141 | 
            +
            									emit = transition.emit
         | 
| 142 | 
            +
            									unless emit == EOS
         | 
| 143 | 
            +
            										bytes = emit.bytes
         | 
| 144 | 
            +
            										fail ArgumentError if bytes.size > 1
         | 
| 145 | 
            +
            										emit = bytes.first
         | 
| 146 | 
            +
            									end
         | 
| 147 | 
            +
            									"[#{emit.inspect}, #{state_id.fetch(transition.node)}]"
         | 
| 148 | 
            +
            								end.join(", ")
         | 
| 149 | 
            +
            								file.print(string)
         | 
| 150 | 
            +
            								file.print "],\n"
         | 
| 151 | 
            +
            							end
         | 
| 152 | 
            +
            							
         | 
| 153 | 
            +
            							file.print <<~FOOTER
         | 
| 154 | 
            +
            											].each {|arr| arr.each {|subarr| subarr.each(&:freeze)}.freeze}.freeze
         | 
| 155 | 
            +
            										end
         | 
| 156 | 
            +
            									end
         | 
| 157 | 
            +
            								end
         | 
| 158 | 
            +
            							FOOTER
         | 
| 159 | 
            +
            						end
         | 
| 160 | 
            +
            					end
         | 
| 161 | 
            +
            					
         | 
| 162 | 
            +
            					class << self
         | 
| 163 | 
            +
            						attr_reader :root
         | 
| 164 | 
            +
            					end
         | 
| 165 | 
            +
            					
         | 
| 166 | 
            +
            					# Test decoder
         | 
| 167 | 
            +
            					def self.decode(input)
         | 
| 168 | 
            +
            						emit = ""
         | 
| 169 | 
            +
            						n = root
         | 
| 170 | 
            +
            						nibbles = input.unpack("C*").flat_map {|b| [((b & 0xf0) >> 4), b & 0xf]}
         | 
| 171 | 
            +
            						until nibbles.empty?
         | 
| 172 | 
            +
            							nb = nibbles.shift
         | 
| 173 | 
            +
            							t = n.transitions[nb]
         | 
| 174 | 
            +
            							emit << t.emit
         | 
| 175 | 
            +
            							n = t.node
         | 
| 176 | 
            +
            						end
         | 
| 177 | 
            +
            						unless n.final && nibbles.all? {|x| x == 0xf}
         | 
| 178 | 
            +
            							puts "len = #{emit.size} n.final = #{n.final} nibbles = #{nibbles}"
         | 
| 179 | 
            +
            						end
         | 
| 180 | 
            +
            						emit
         | 
| 181 | 
            +
            					end
         | 
| 182 | 
            +
            				end
         | 
| 183 | 
            +
            			end
         | 
| 184 | 
            +
            		end
         | 
| 185 | 
            +
            	end
         | 
| 186 | 
            +
            end
         |