http-2 0.6.1 → 0.6.3

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.
@@ -19,6 +19,4 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_development_dependency "bundler", "~> 1.3"
22
- spec.add_development_dependency "rake"
23
- spec.add_development_dependency "rspec"
24
22
  end
@@ -6,4 +6,6 @@ require "http/2/flow_buffer"
6
6
  require "http/2/compressor"
7
7
  require "http/2/framer"
8
8
  require "http/2/connection"
9
+ require "http/2/client"
10
+ require "http/2/server"
9
11
  require "http/2/stream"
@@ -4,17 +4,34 @@ module HTTP2
4
4
  #
5
5
  class Buffer < String
6
6
 
7
+ UINT32 = "N"
8
+ BINARY = "binary"
9
+ private_constant :UINT32, :BINARY
10
+
7
11
  # Forces binary encoding on the string
8
- def initialize(*args)
9
- force_encoding('binary')
10
- super(*args)
12
+ def initialize(data = '')
13
+ super(data.force_encoding(BINARY))
11
14
  end
12
15
 
13
16
  # Emulate StringIO#read: slice first n bytes from the buffer.
14
17
  #
15
18
  # @param n [Integer] number of bytes to slice from the buffer
16
19
  def read(n)
17
- slice!(0,n)
20
+ Buffer.new(slice!(0,n))
21
+ end
22
+
23
+ # Alias getbyte to readbyte
24
+ alias :readbyte :getbyte
25
+
26
+ # Emulate StringIO#getbyte: slice first byte from buffer.
27
+ def getbyte
28
+ read(1).ord
29
+ end
30
+
31
+ # Slice unsigned 32-bit integer from buffer.
32
+ # @return [Integer]
33
+ def read_uint32
34
+ read(4).unpack(UINT32).first
18
35
  end
19
36
  end
20
37
 
@@ -0,0 +1,50 @@
1
+ module HTTP2
2
+
3
+ # HTTP 2.0 client connection class that implements appropriate header
4
+ # compression / decompression algorithms and stream management logic.
5
+ #
6
+ # Your code is responsible for driving the client object, which in turn
7
+ # performs all of the necessary HTTP 2.0 encoding / decoding, state
8
+ # management, and the rest. A simple example:
9
+ #
10
+ # @example
11
+ # socket = YourTransport.new
12
+ #
13
+ # conn = HTTP2::Client.new
14
+ # conn.on(:frame) {|bytes| socket << bytes }
15
+ #
16
+ # while bytes = socket.read
17
+ # conn << bytes
18
+ # end
19
+ #
20
+ class Client < Connection
21
+
22
+ # Initialize new HTTP 2.0 client object.
23
+ def initialize(*args)
24
+ @stream_id = 1
25
+ @state = :connection_header
26
+ @compressor = Header::Compressor.new(:request)
27
+ @decompressor = Header::Decompressor.new(:response)
28
+
29
+ super
30
+ end
31
+
32
+ # Send an outgoing frame. Connection and stream flow control is managed
33
+ # by Connection class.
34
+ #
35
+ # @see Connection
36
+ # @note Client will emit the connection header as the first 24 bytes
37
+ # @param frame [Hash]
38
+ def send(frame)
39
+ if @state == :connection_header
40
+ emit(:frame, CONNECTION_HEADER)
41
+ @state = :connected
42
+
43
+ settings(stream_limit: @stream_limit, window_limit: @window_limit)
44
+ end
45
+
46
+ super(frame)
47
+ end
48
+ end
49
+
50
+ end
@@ -1,5 +1,3 @@
1
- require "stringio"
2
-
3
1
  module HTTP2
4
2
 
5
3
  # Implementation of header compression for HTTP 2.0 (HPACK) format adapted
@@ -12,97 +10,84 @@ module HTTP2
12
10
  # encoding context: an encoding context contains a header table and a
13
11
  # reference set - there is one encoding context for each direction.
14
12
  #
15
- class CompressionContext
13
+ class EncodingContext
16
14
  include Error
17
15
 
18
16
  # TODO: replace StringIO with Buffer...
19
17
 
20
18
  # Default request working set as defined by the spec.
21
19
  REQ_DEFAULTS = [
22
- [':scheme' ,'http' ],
23
- [':scheme' ,'https'],
24
- [':host' ,'' ],
25
- [':path' ,'/' ],
26
- [':method' ,'get' ],
27
- ['accept' ,'' ],
28
- ['accept-charset' ,'' ],
29
- ['accept-encoding' ,'' ],
30
- ['accept-language' ,'' ],
31
- ['cookie' ,'' ],
32
- ['if-modified-since' ,'' ],
33
- ['keep-alive' ,'' ],
34
- ['user-agent' ,'' ],
35
- ['proxy-connection' ,'' ],
36
- ['referer' ,'' ],
37
- ['accept-datetime' ,'' ],
38
- ['authorization' ,'' ],
39
- ['allow' ,'' ],
40
- ['cache-control' ,'' ],
41
- ['connection' ,'' ],
42
- ['content-length' ,'' ],
43
- ['content-md5' ,'' ],
44
- ['content-type' ,'' ],
45
- ['date' ,'' ],
46
- ['expect' ,'' ],
47
- ['from' ,'' ],
48
- ['if-match' ,'' ],
49
- ['if-none-match' ,'' ],
50
- ['if-range' ,'' ],
51
- ['if-unmodified-since','' ],
52
- ['max-forwards' ,'' ],
53
- ['pragma' ,'' ],
54
- ['proxy-authorization','' ],
55
- ['range' ,'' ],
56
- ['te' ,'' ],
57
- ['upgrade' ,'' ],
58
- ['via' ,'' ],
59
- ['warning' ,'' ]
60
- ];
20
+ [':scheme' , 'http' ],
21
+ [':scheme' , 'https'],
22
+ [':host' , '' ],
23
+ [':path' , '/' ],
24
+ [':method' , 'get' ],
25
+ ['accept' , '' ],
26
+ ['accept-charset' , '' ],
27
+ ['accept-encoding' , '' ],
28
+ ['accept-language' , '' ],
29
+ ['cookie' , '' ],
30
+ ['if-modified-since' , '' ],
31
+ ['user-agent' , '' ],
32
+ ['referer' , '' ],
33
+ ['authorization' , '' ],
34
+ ['allow' , '' ],
35
+ ['cache-control' , '' ],
36
+ ['connection' , '' ],
37
+ ['content-length' , '' ],
38
+ ['content-type' , '' ],
39
+ ['date' , '' ],
40
+ ['expect' , '' ],
41
+ ['from' , '' ],
42
+ ['if-match' , '' ],
43
+ ['if-none-match' , '' ],
44
+ ['if-range' , '' ],
45
+ ['if-unmodified-since', '' ],
46
+ ['max-forwards' , '' ],
47
+ ['proxy-authorization', '' ],
48
+ ['range' , '' ],
49
+ ['via' , '' ]
50
+ ]
61
51
 
62
52
  # Default response working set as defined by the spec.
63
53
  RESP_DEFAULTS = [
64
- [':status' ,'200'],
65
- ['age' ,'' ],
66
- ['cache-control' ,'' ],
67
- ['content-length' ,'' ],
68
- ['content-type' ,'' ],
69
- ['date' ,'' ],
70
- ['etag' ,'' ],
71
- ['expires' ,'' ],
72
- ['last-modified' ,'' ],
73
- ['server' ,'' ],
74
- ['set-cookie' ,'' ],
75
- ['vary' ,'' ],
76
- ['via' ,'' ],
77
- ['access-control-allow-origin','' ],
78
- ['accept-ranges' ,'' ],
79
- ['allow' ,'' ],
80
- ['connection' ,'' ],
81
- ['content-disposition' ,'' ],
82
- ['content-encoding' ,'' ],
83
- ['content-language' ,'' ],
84
- ['content-location' ,'' ],
85
- ['content-md5' ,'' ],
86
- ['content-range' ,'' ],
87
- ['link' ,'' ],
88
- ['location' ,'' ],
89
- ['p3p' ,'' ],
90
- ['pragma' ,'' ],
91
- ['proxy-authenticate' ,'' ],
92
- ['refresh' ,'' ],
93
- ['retry-after' ,'' ],
94
- ['strict-transport-security' ,'' ],
95
- ['trailer' ,'' ],
96
- ['transfer-encoding' ,'' ],
97
- ['warning' ,'' ],
98
- ['www-authenticate' ,'' ]
99
- ];
54
+ [':status' , '200'],
55
+ ['age' , '' ],
56
+ ['cache-control' , '' ],
57
+ ['content-length' , '' ],
58
+ ['content-type' , '' ],
59
+ ['date' , '' ],
60
+ ['etag' , '' ],
61
+ ['expires' , '' ],
62
+ ['last-modified' , '' ],
63
+ ['server' , '' ],
64
+ ['set-cookie' , '' ],
65
+ ['vary' , '' ],
66
+ ['via' , '' ],
67
+ ['access-control-allow-origin' , '' ],
68
+ ['accept-ranges' , '' ],
69
+ ['allow' , '' ],
70
+ ['connection' , '' ],
71
+ ['content-disposition' , '' ],
72
+ ['content-encoding' , '' ],
73
+ ['content-language' , '' ],
74
+ ['content-location' , '' ],
75
+ ['content-range' , '' ],
76
+ ['link' , '' ],
77
+ ['location' , '' ],
78
+ ['proxy-authenticate' , '' ],
79
+ ['refresh' , '' ],
80
+ ['retry-after' , '' ],
81
+ ['strict-transport-security' , '' ],
82
+ ['transfer-encoding' , '' ],
83
+ ['www-authenticate' , '' ]
84
+ ]
100
85
 
101
86
  # Current table of header key-value pairs.
102
87
  attr_reader :table
103
88
 
104
- # Current working set of header key-value pairs.
105
- attr_reader :workset
89
+ # Current reference set of header key-value pairs.
90
+ attr_reader :refset
106
91
 
107
92
  # Initializes compression context with appropriate client/server
108
93
  # defaults and maximum size of the header table.
@@ -113,37 +98,50 @@ module HTTP2
113
98
  @type = type
114
99
  @table = (type == :request) ? REQ_DEFAULTS.dup : RESP_DEFAULTS.dup
115
100
  @limit = limit
116
- @workset = []
101
+ @refset = []
117
102
  end
118
103
 
119
104
  # Performs differential coding based on provided command type.
120
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#section-3.1
105
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-3.2
121
106
  #
122
107
  # @param cmd [Hash]
108
+ # @return [Hash] emitted header
123
109
  def process(cmd)
110
+ emit = nil
111
+
124
112
  # indexed representation
125
113
  if cmd[:type] == :indexed
126
- # For an indexed representation, the decoder checks whether the index
127
- # is present in the working set. If true, the corresponding entry is
128
- # removed from the working set. If several entries correspond to this
129
- # encoded index, all these entries are removed from the working set.
130
- # If the index is not present in the working set, it is used to
131
- # retrieve the corresponding header from the header table, and a new
132
- # entry is added to the working set representing this header.
133
- cur = @workset.find_index {|(i,v)| i == cmd[:name]}
114
+ # An indexed representation corresponding to an entry not present
115
+ # in the reference set entails the following actions:
116
+ # - The header corresponding to the entry is emitted.
117
+ # - The entry is added to the reference set.
118
+ #
119
+ # An indexed representation corresponding to an entry present in
120
+ # the reference set entails the following actions:
121
+ # - The entry is removed from the reference set.
122
+ #
123
+ idx = cmd[:name]
124
+ cur = @refset.find_index {|(i,v)| i == idx}
134
125
 
135
126
  if cur
136
- @workset.delete_at(cur)
127
+ @refset.delete_at(cur)
137
128
  else
138
- @workset.push [cmd[:name], @table[cmd[:name]]]
129
+ emit = @table[idx]
130
+ @refset.push [idx, @table[idx]]
139
131
  end
140
132
 
141
133
  else
142
- # For a literal representation, a new entry is added to the working
143
- # set representing this header. If the literal representation specifies
144
- # that the header is to be indexed, the header is added accordingly to
145
- # the header table, and its index is included in the entry in the working
146
- # set. Otherwise, the entry in the working set contains an undefined index.
134
+ # A literal representation that is not added to the header table
135
+ # entails the following action:
136
+ # - The header is emitted.
137
+ #
138
+ # A literal representation that is added to the header table entails
139
+ # the following actions:
140
+ # - The header is emitted.
141
+ # - The header is added to the header table, at the location
142
+ # defined by the representation.
143
+ # - The new entry is added to the reference set.
144
+ #
147
145
  if cmd[:name].is_a? Integer
148
146
  k,v = @table[cmd[:name]]
149
147
 
@@ -152,42 +150,29 @@ module HTTP2
152
150
  cmd[:name] = k
153
151
  end
154
152
 
155
- newval = [cmd[:name], cmd[:value]]
153
+ emit = [cmd[:name], cmd[:value]]
156
154
 
157
155
  if cmd[:type] != :noindex
158
- size_check cmd
159
-
160
- case cmd[:type]
161
- when :incremental
162
- cmd[:index] = @table.size
163
- when :substitution
164
- if @table[cmd[:index]].nil?
165
- raise HeaderException.new("invalid index")
156
+ if size_check(cmd)
157
+
158
+ case cmd[:type]
159
+ when :incremental
160
+ cmd[:index] = @table.size
161
+ when :substitution
162
+ if @table[cmd[:index]].nil?
163
+ raise HeaderException.new("invalid index")
164
+ end
165
+ when :prepend
166
+ @table = [emit] + @table
166
167
  end
167
- when :prepend
168
- @table = [newval] + @table
169
- end
170
168
 
171
- @table[cmd[:index]] = newval
169
+ @table[cmd[:index]] = emit
170
+ @refset.push [cmd[:index], emit]
171
+ end
172
172
  end
173
-
174
- @workset.push [cmd[:index], newval]
175
173
  end
176
- end
177
174
 
178
- # First, upon starting the decoding of a new set of headers, the
179
- # reference set of headers is interpreted into the working set of
180
- # headers: for each header in the reference set, an entry is added to
181
- # the working set, containing the header name, its value, and its
182
- # current index in the header table.
183
- #
184
- # @return [Array] current working set
185
- def update_sets
186
- # new refset is the the workset sans headers not in header table
187
- refset = @workset.reject {|(i,h)| !@table.include? h}
188
-
189
- # new workset is the refset with index of each header in header table
190
- @workset = refset.collect {|(i,h)| [@table.find_index(h), h]}
175
+ emit
191
176
  end
192
177
 
193
178
  # Emits best available command to encode provided header.
@@ -230,40 +215,55 @@ module HTTP2
230
215
 
231
216
  private
232
217
 
233
- # Before adding a new entry to the header table or changing an existing
234
- # one, a check has to be performed to ensure that the change will not
235
- # cause the table to grow in size beyond the SETTINGS_MAX_BUFFER_SIZE
236
- # limit. If necessary, one or more items from the beginning of the
237
- # table are removed until there is enough free space available to make
238
- # the modification. Dropping an entry from the beginning of the table
239
- # causes the index positions of the remaining entries in the table to
240
- # be decremented by 1.
218
+ # Before doing such a modification, it has to be ensured that the header
219
+ # table size will stay lower than or equal to the
220
+ # SETTINGS_HEADER_TABLE_SIZE limit. To achieve this, repeatedly, the
221
+ # first entry of the header table is removed, until enough space is
222
+ # available for the modification.
223
+ #
224
+ # A consequence of removing one or more entries at the beginning of the
225
+ # header table is that the remaining entries are renumbered. The first
226
+ # entry of the header table is always associated to the index 0.
241
227
  #
242
228
  # @param cmd [Hash]
229
+ # @return [Boolean]
243
230
  def size_check(cmd)
244
231
  cursize = @table.join.bytesize + @table.size * 32
245
232
  cmdsize = cmd[:name].bytesize + cmd[:value].bytesize + 32
246
233
 
234
+ # The addition of a new entry with a size greater than the
235
+ # SETTINGS_HEADER_TABLE_SIZE limit causes all the entries from the
236
+ # header table to be dropped and the new entry not to be added to the
237
+ # header table. The replacement of an existing entry with a new entry
238
+ # with a size greater than the SETTINGS_HEADER_TABLE_SIZE has the same
239
+ # consequences.
240
+ if cmdsize > @limit
241
+ @table.clear
242
+ return false
243
+ end
244
+
247
245
  cur = 0
248
246
  while (cursize + cmdsize) > @limit do
249
247
  e = @table.shift
250
248
 
251
- # When using substitution indexing, it is possible that the existing
252
- # item being replaced might be one of the items removed when performing
253
- # the necessary size adjustment. In such cases, the substituted value
254
- # being added to the header table is inserted at the beginning of the
255
- # header table (at index position #0) and the index positions of the
256
- # other remaining entries in the table are incremented by 1.
249
+ # When the modification of the header table is the replacement of an
250
+ # existing entry, the replaced entry is the one indicated in the
251
+ # literal representation before any entry is removed from the header
252
+ # table. If the entry to be replaced is removed from the header table
253
+ # when performing the size adjustment, the replacement entry is
254
+ # inserted at the beginning of the header table.
257
255
  if cmd[:type] == :substitution && cur == cmd[:index]
258
256
  cmd[:type] = :prepend
259
257
  end
260
258
 
261
259
  cursize -= (e.join.bytesize + 32)
262
260
  end
261
+
262
+ return true
263
263
  end
264
264
 
265
265
  def active?(idx)
266
- !@workset.find {|i,_| i == idx }.nil?
266
+ !@refset.find {|i,_| i == idx }.nil?
267
267
  end
268
268
 
269
269
  def default?(idx)
@@ -289,20 +289,20 @@ module HTTP2
289
289
  # server_role = Compressor.new(:response)
290
290
  class Compressor
291
291
  def initialize(type)
292
- @cc = CompressionContext.new(type)
292
+ @cc = EncodingContext.new(type)
293
293
  end
294
294
 
295
295
  # Encodes provided value via integer representation.
296
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#section-4.2.1
296
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-4.1.1
297
297
  #
298
- # If I < 2^N - 1, encode I on N bits
299
- # Else, encode 2^N - 1 on N bits and do the following steps:
300
- # Set I to (I - (2^N - 1)) and Q to 1
301
- # While Q > 0
302
- # Compute Q and R, quotient and remainder of I divided by 2^7
303
- # If Q is strictly greater than 0, write one 1 bit; otherwise, write one 0 bit
304
- # Encode R on the next 7 bits
305
- # I = Q
298
+ # If I < 2^N - 1, encode I on N bits
299
+ # Else
300
+ # encode 2^N - 1 on N bits
301
+ # I = I - (2^N - 1)
302
+ # While I >= 128
303
+ # Encode (I % 128 + 128) on 8 bits
304
+ # I = I / 128
305
+ # encode (I) on 8 bits
306
306
  #
307
307
  # @param i [Integer] value to encode
308
308
  # @param n [Integer] number of available bits
@@ -315,21 +315,17 @@ module HTTP2
315
315
  bytes.push limit if !n.zero?
316
316
 
317
317
  i -= limit
318
- q = 1
319
-
320
- while (q > 0) do
321
- q, r = i.divmod(128)
322
- r += 128 if (q > 0)
323
- i = q
324
-
325
- bytes.push(r)
318
+ while (i >= 128) do
319
+ bytes.push((i % 128) + 128)
320
+ i = i / 128
326
321
  end
327
322
 
323
+ bytes.push i
328
324
  bytes.pack('C*')
329
325
  end
330
326
 
331
327
  # Encodes provided value via string literal representation.
332
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#section-4.2.2
328
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-4.1.3
333
329
  #
334
330
  # * The string length, defined as the number of bytes needed to store
335
331
  # its UTF-8 representation, is represented as an integer with a zero
@@ -340,15 +336,15 @@ module HTTP2
340
336
  # @param str [String]
341
337
  # @return [String] binary string
342
338
  def string(str)
343
- integer(str.bytesize, 0) + str.dup.force_encoding('binary')
339
+ integer(str.bytesize, 0) << str.dup.force_encoding('binary')
344
340
  end
345
341
 
346
342
  # Encodes header command with appropriate header representation.
347
- # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-01#section-4.3
343
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-4
348
344
  #
349
345
  # @param h [Hash] header command
350
346
  # @param buffer [String]
351
- def header(h, buffer = "")
347
+ def header(h, buffer = Buffer.new)
352
348
  rep = HEADREP[h[:type]]
353
349
 
354
350
  if h[:type] == :indexed
@@ -383,29 +379,35 @@ module HTTP2
383
379
  # Encodes provided list of HTTP headers.
384
380
  #
385
381
  # @param headers [Hash]
386
- # @return [String] binary string
382
+ # @return [Buffer]
387
383
  def encode(headers)
384
+ buffer = Buffer.new
388
385
  commands = []
389
- @cc.update_sets
390
386
 
391
- # Remove missing headers from the working set
392
- @cc.workset.each do |idx, (wk,wv)|
387
+ # Literal header names MUST be translated to lowercase before
388
+ # encoding and transmission.
389
+ headers.map! {|(hk,hv)| [hk.downcase, hv] }
390
+
391
+ # Generate remove commands for missing headers
392
+ @cc.refset.each do |idx, (wk,wv)|
393
393
  if headers.find {|(hk,hv)| hk == wk && hv == wv }.nil?
394
394
  commands.push @cc.removecmd idx
395
395
  end
396
396
  end
397
397
 
398
- # Add missing headers to the working set
398
+ # Generate add commands for new headers
399
399
  headers.each do |(hk,hv)|
400
- if @cc.workset.find {|i,(wk,wv)| hk == wk && hv == wv}.nil?
400
+ if @cc.refset.find {|i,(wk,wv)| hk == wk && hv == wv}.nil?
401
401
  commands.push @cc.addcmd [hk, hv]
402
402
  end
403
403
  end
404
404
 
405
- commands.map do |cmd|
405
+ commands.each do |cmd|
406
406
  @cc.process cmd.dup
407
- header cmd
408
- end.join
407
+ buffer << header(cmd)
408
+ end
409
+
410
+ buffer
409
411
  end
410
412
  end
411
413
 
@@ -418,7 +420,7 @@ module HTTP2
418
420
  # client_role = Decompressor.new(:response)
419
421
  class Decompressor
420
422
  def initialize(type)
421
- @cc = CompressionContext.new(type)
423
+ @cc = EncodingContext.new(type)
422
424
  end
423
425
 
424
426
  # Decodes integer value from provided buffer.
@@ -430,7 +432,7 @@ module HTTP2
430
432
  i = !n.zero? ? (buf.getbyte & limit) : 0
431
433
 
432
434
  m = 0
433
- buf.each_byte do |byte|
435
+ while byte = buf.getbyte do
434
436
  i += ((byte & 127) << m)
435
437
  m += 7
436
438
 
@@ -450,10 +452,9 @@ module HTTP2
450
452
 
451
453
  # Decodes header command from provided buffer.
452
454
  #
453
- # @param buf [String]
455
+ # @param buf [Buffer]
454
456
  def header(buf)
455
- peek = buf.getbyte
456
- buf.seek(-1, IO::SEEK_CUR)
457
+ peek = buf.readbyte(0)
457
458
 
458
459
  header = {}
459
460
  header[:type], type = HEADREP.select do |t, desc|
@@ -481,11 +482,26 @@ module HTTP2
481
482
 
482
483
  # Decodes and processes header commands within provided buffer.
483
484
  #
484
- # @param buf [String]
485
+ # Once all the representations contained in a header block have been
486
+ # processed, the headers that are in common with the previous header
487
+ # set are emitted, during the reference set emission.
488
+ #
489
+ # For the reference set emission, each header contained in the
490
+ # reference set that has not been emitted during the processing of the
491
+ # header block is emitted.
492
+ #
493
+ # - http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-3.2.2
494
+ #
495
+ # @param buf [Buffer]
496
+ # @return [Array] set of HTTP headers
485
497
  def decode(buf)
486
- @cc.update_sets
487
- @cc.process(header(buf)) while !buf.eof?
488
- @cc.workset.map {|i,header| header}
498
+ set = []
499
+ set << @cc.process(header(buf)) while !buf.empty?
500
+ @cc.refset.each do |i,header|
501
+ set << header if !set.include? header
502
+ end
503
+
504
+ set.compact
489
505
  end
490
506
  end
491
507