File: generator.rb

package info (click to toggle)
ruby-protocol-hpack 1.5.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 196 kB
  • sloc: ruby: 887; makefile: 4
file content (186 lines) | stat: -rw-r--r-- 4,530 bytes parent folder | download
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
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
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2014, by Kaoru Maeda.
# Copyright, 2015, by Tamir Duberstein.
# Copyright, 2015, by Ilya Grigorik.
# Copyright, 2016, by George Ulmer.
# Copyright, 2018-2024, by Samuel Williams.
# Copyright, 2024, by Nathan Froyd.

require_relative "../huffman"
require "set"

module Protocol
	module HPACK
		class Huffman
			module Generator
				EOS = 256
				
				class Node
					attr_accessor :next, :emit, :final, :depth
					attr_accessor :transitions
					attr_accessor :id
					
					@@id = 0
					
					def initialize(depth)
						@next = [nil, nil]
						@id = @@id
						@@id += 1
						@final = false
						@depth = depth
					end
					
					def add(code, len, chr)
						self.final = true if chr == EOS && @depth <= 7
						if len.zero?
							@emit = chr
						else
							bit = (code & (1 << (len - 1))).zero? ? 0 : 1
							node = @next[bit] ||= Node.new(@depth + 1)
							node.add(code, len - 1, chr)
						end
					end
					
					class Transition
						attr_accessor :emit, :node
						def initialize(emit, node)
							@emit = emit
							@node = node
						end
					end
					
					def self.generate_tree
						@root = new(0)
						Protocol::HPACK::Huffman::CODES.each_with_index do |c, chr|
							code, len = c
							@root.add(code, len, chr)
						end
						puts "#{@@id} nodes"
						@root
					end
					
					def self.generate_machine
						generate_tree
						
						# Using un-ordered sets (potentially) produces non-deterministic results:
						togo = Set[@root]
						@states = Set[@root]
						
						until togo.empty?
							node = togo.first
							togo.delete(node)
							
							next if node.transitions
							node.transitions = Array[1 << BITS_AT_ONCE]
							
							(1 << BITS_AT_ONCE).times do |input|
								n = node
								emit = +""
								(BITS_AT_ONCE - 1).downto(0) do |i|
									bit = (input & (1 << i)).zero? ? 0 : 1
									n = n.next[bit]
									next unless n.emit
									if n.emit == EOS
										emit = EOS # cause error on decoding
									else
										emit << n.emit.chr(Encoding::BINARY) unless emit == EOS
									end
									n = @root
								end
								node.transitions[input] = Transition.new(emit, n)
								togo << n
								@states << n
							end
						end
						puts "#{@states.size} states"
						@root
					end
					
					MACHINE_PATH = File.expand_path("machine.rb", __dir__)
					
					def self.generate_state_table(output_path = MACHINE_PATH)
						generate_machine
						state_id = {}
						id_state = {}
						state_id[@root] = 0
						id_state[0] = @root
						max_final = 0
						id = 1
						(@states - [@root]).sort_by {|s| s.final ? 0 : 1}.each do |s|
							state_id[s] = id
							id_state[id] = s
							max_final = id if s.final
							id += 1
						end

						File.open(output_path, "w") do |file|
							file.print <<~HEADER
								# frozen_string_literal: true

								# Released under the MIT License.
								# Copyright, 2018-2024, by Samuel Williams.

								# Machine generated Huffman decoder state machine.
								# DO NOT EDIT THIS FILE.

								module Protocol
									module HPACK
										class Huffman
											# :nodoc:
											MAX_FINAL_STATE = #{max_final}
											MACHINE = [
							HEADER
							
							id.times do |i|
								n = id_state[i]
								file.print "\t\t\t\t["
								string = (1 << BITS_AT_ONCE).times.map do |t|
									transition = n.transitions.fetch(t)
									emit = transition.emit
									unless emit == EOS
										bytes = emit.bytes
										fail ArgumentError if bytes.size > 1
										emit = bytes.first
									end
									"[#{emit.inspect}, #{state_id.fetch(transition.node)}]"
								end.join(", ")
								file.print(string)
								file.print "],\n"
							end
							
							file.print <<~FOOTER
											].each {|arr| arr.each {|subarr| subarr.each(&:freeze)}.freeze}.freeze
										end
									end
								end
							FOOTER
						end
					end
					
					class << self
						attr_reader :root
					end
					
					# Test decoder
					def self.decode(input)
						emit = ""
						n = root
						nibbles = input.unpack("C*").flat_map {|b| [((b & 0xf0) >> 4), b & 0xf]}
						until nibbles.empty?
							nb = nibbles.shift
							t = n.transitions[nb]
							emit << t.emit
							n = t.node
						end
						unless n.final && nibbles.all? {|x| x == 0xf}
							puts "len = #{emit.size} n.final = #{n.final} nibbles = #{nibbles}"
						end
						emit
					end
				end
			end
		end
	end
end