skyfall 0.6.1 → 0.7.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.
@@ -1,17 +1,85 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'eventmachine'
2
4
  require 'faye/websocket'
3
5
  require 'uri'
4
6
 
7
+ require_relative 'errors'
8
+ require_relative 'events'
5
9
  require_relative 'version'
6
10
 
7
11
  module Skyfall
12
+
13
+ # Base class of a websocket client. It provides basic websocket client functionality such as
14
+ # connecting to the service, keeping the connection alive and running lifecycle callbacks.
15
+ #
16
+ # In most cases, you will not create instances of this class directly, but rather use either
17
+ # {Firehose} or {Jetstream}. Use this class as a superclass if you need to implement some
18
+ # custom client for a websocket API that isn't supported yet.
19
+
8
20
  class Stream
9
- EVENTS = %w(message raw_message connecting connect disconnect reconnect error timeout)
10
- MAX_RECONNECT_INTERVAL = 300
21
+ extend Events
11
22
 
12
- attr_accessor :auto_reconnect, :last_update, :user_agent
13
- attr_accessor :heartbeat_timeout, :heartbeat_interval, :check_heartbeat
23
+ MAX_RECONNECT_INTERVAL = 300
14
24
 
25
+ # If enabled, the client will try to reconnect if the connection is closed unexpectedly.
26
+ # (Default: true)
27
+ #
28
+ # When the reconnect attempt fails, it will wait with an exponential backoff delay before
29
+ # retrying again, up to {MAX_RECONNECT_INTERVAL} seconds.
30
+ #
31
+ # @return [Boolean]
32
+ attr_accessor :auto_reconnect
33
+
34
+ # User agent sent in the header when connecting.
35
+ #
36
+ # Default value is {#default_user_agent} = {#version_string} `(Skyfall/x.y)`. It's recommended
37
+ # to set it or extend it with some information that indicates what service this is and who is
38
+ # running it (e.g. a Bluesky handle).
39
+ #
40
+ # @return [String]
41
+ # @example
42
+ # client.user_agent = "my.service (@my.handle) #{client.version_string}"
43
+ attr_accessor :user_agent
44
+
45
+ # If enabled, runs a timer which does periodical "heatbeat checks".
46
+ #
47
+ # The heartbeat timer is started when the client connects to the service, and checks if the stream
48
+ # hasn't stalled and is still regularly sending new messages. If no messages are detected for some
49
+ # period of time, the client forces a reconnect.
50
+ #
51
+ # This is **not** enabled by default, because depending on the service you're connecting to, it
52
+ # might be normal to not receive any messages for a while.
53
+ #
54
+ # @see #heartbeat_timeout
55
+ # @see #heartbeat_interval
56
+ # @return [Boolean]
57
+ attr_accessor :check_heartbeat
58
+
59
+ # Interval in seconds between heartbeat checks (default: 10). Only used if {#check_heartbeat} is set.
60
+ # @return [Numeric]
61
+ attr_accessor :heartbeat_interval
62
+
63
+ # Number of seconds without messages after which reconnect is triggered (default: 300).
64
+ # Only used if {#check_heartbeat} is set.
65
+ # @return [Numeric]
66
+ attr_accessor :heartbeat_timeout
67
+
68
+ # Time when the most recent message was received from the websocket.
69
+ #
70
+ # Note: this is _local time_ when the message was received; this is different from the timestamp
71
+ # of the message, which is the server time of the original source (PDS) when emitting the message,
72
+ # and different from a potential `created_at` saved in the record.
73
+ #
74
+ # @return [Time, nil]
75
+ attr_reader :last_update
76
+
77
+ #
78
+ # @param server [String] Address of the server to connect to.
79
+ # Expects a string with either just a hostname, or a ws:// or wss:// URL.
80
+ #
81
+ # @raise [ArgumentError] if the server parameter is invalid
82
+ #
15
83
  def initialize(server)
16
84
  @root_url = build_root_url(server)
17
85
 
@@ -27,9 +95,23 @@ module Skyfall
27
95
  @handlers[:error] = proc { |e| puts "ERROR: #{e}" }
28
96
  end
29
97
 
98
+ #
99
+ # Opens a connection to the configured websocket.
100
+ #
101
+ # This method starts an EventMachine reactor on the current thread, and will only return
102
+ # once the connection is closed.
103
+ #
104
+ # @return [nil]
105
+ # @raise [ConfigError] if no message handler has been configured
106
+ # @raise [ReactorActiveError] if another stream is already running
107
+ #
30
108
  def connect
31
109
  return if @ws
32
110
 
111
+ if @handlers[:message].nil? && @handlers[:raw_message].nil?
112
+ raise ConfigError, "Either on_message or on_raw_message handler needs to be set"
113
+ end
114
+
33
115
  url = build_websocket_url
34
116
 
35
117
  @handlers[:connecting]&.call(url)
@@ -86,11 +168,10 @@ module Skyfall
86
168
  end
87
169
  end
88
170
 
89
- def handle_message(msg)
90
- data = msg.data
91
- @handlers[:raw_message]&.call(data)
92
- end
93
-
171
+ #
172
+ # Forces a reconnect, closing the connection and calling {#connect} again.
173
+ # @return [nil]
174
+ #
94
175
  def reconnect
95
176
  @reconnecting = true
96
177
  @connection_attempts = 0
@@ -98,6 +179,10 @@ module Skyfall
98
179
  @ws ? @ws.close : connect
99
180
  end
100
181
 
182
+ #
183
+ # Closes the connection and stops the EventMachine reactor thread.
184
+ # @return [nil]
185
+ #
101
186
  def disconnect
102
187
  return unless EM.reactor_running?
103
188
 
@@ -108,10 +193,18 @@ module Skyfall
108
193
 
109
194
  alias close disconnect
110
195
 
196
+ #
197
+ # Default user agent sent when connecting to the service. (Currently `"#{version_string}"`)
198
+ # @return [String]
199
+ #
111
200
  def default_user_agent
112
201
  version_string
113
202
  end
114
203
 
204
+ #
205
+ # Skyfall version string for use in user agent strings (`"Skyfall/x.y"`).
206
+ # @return [String]
207
+ #
115
208
  def version_string
116
209
  "Skyfall/#{Skyfall::VERSION}"
117
210
  end
@@ -126,60 +219,172 @@ module Skyfall
126
219
  end
127
220
  end
128
221
 
129
- def start_heartbeat_timer
130
- return if !@check_heartbeat || @heartbeat_interval.to_f <= 0 || @heartbeat_timeout.to_f <= 0
131
- return if @heartbeat_timer
132
222
 
133
- @heartbeat_timer = EM::PeriodicTimer.new(@heartbeat_interval) do
134
- next if @ws.nil? || @heartbeat_timeout.to_f <= 0
135
- time_passed = Time.now - @last_update
136
-
137
- if time_passed > @heartbeat_timeout
138
- @handlers[:timeout]&.call
139
- reconnect
140
- end
141
- end
223
+ # @!method on_connecting(block)
224
+ # Defines a callback to be run when the client tries to open a connection to the websocket.
225
+ # Can be also run as a setter `on_connecting=`.
226
+ # @param [Proc] block
227
+ # @yieldparam [String] url URL to which the client is connecting
228
+ # @return [nil]
229
+
230
+ event_handler :connecting
231
+
232
+ # @!method on_connect(block)
233
+ # Defines a callback to be run after a connection to the websocket is opened.
234
+ # Can be also run as a setter `on_connect=`.
235
+ # @param [Proc] block
236
+ # @return [nil]
237
+
238
+ event_handler :connect
239
+
240
+ # @!method on_raw_message(block)
241
+ # Defines a callback to be run when a message is received, passing a raw data packet as
242
+ # received from the websocket (plain text or binary). Can be also run as a setter `on_raw_message=`.
243
+ # @param [Proc] block
244
+ # @yieldparam [String] data payload of the received message
245
+ # @return [nil]
246
+
247
+ event_handler :raw_message
248
+
249
+ # @!method on_message(block)
250
+ # Defines a callback to be run when a message is received, passing the message as a parsed
251
+ # object of an appropriate message class. Can be also run as a setter `on_message=`.
252
+ # @param [Proc] block
253
+ # @yieldparam [Object] message parsed message of an appropriate class
254
+ # @return [nil]
255
+
256
+ event_handler :message
257
+
258
+ # @!method on_disconnect(block)
259
+ # Defines a callback to be run after a connection to the websocket is closed (and the client
260
+ # does not reconnect). Can be also run as a setter `on_disconnect=`.
261
+ #
262
+ # This callback is not run when `on_reconnect` fires.
263
+ # @param [Proc] block
264
+ # @return [nil]
265
+
266
+ event_handler :disconnect
267
+
268
+ # @!method on_reconnect(block)
269
+ # Defines a callback to be run when a connection to the websocket is broken, but the client
270
+ # initiates or schedules a reconnect (which may happen after a delay). Can be also run as
271
+ # a setter `on_reconnect=`.
272
+ # @param [Proc] block
273
+ # @return [nil]
274
+
275
+ event_handler :reconnect
276
+
277
+ # @!method on_timeout(block)
278
+ # Defines a callback to be run when the heartbeat timer forces a reconnect. A reconnect is
279
+ # triggered after not receiving any messages for a period of time specified in {#heartbeat_timeout}
280
+ # (if {#check_heartbeat} is enabled). Can be also run as a setter `on_timeout=`.
281
+ #
282
+ # This callback is also followed by `on_reconnect`.
283
+ # @param [Proc] block
284
+ # @return [nil]
285
+
286
+ event_handler :timeout
287
+
288
+ # @!method on_error(block)
289
+ # Defines a callback to be run when the websocket connection returns an error. Can be also
290
+ # run as a setter `on_error=`.
291
+ #
292
+ # Default handler prints the error to stdout.
293
+ #
294
+ # @param [Proc] block
295
+ # @yieldparam [Exception] error the received error
296
+ # @return [nil]
297
+
298
+ event_handler :error
299
+
300
+
301
+ # Returns a string with a representation of the object for debugging purposes.
302
+ # @return [String]
303
+ def inspect
304
+ vars = inspectable_variables.map { |v| "#{v}=#{instance_variable_get(v).inspect}" }.join(", ")
305
+ "#<#{self.class}:0x#{object_id} #{vars}>"
142
306
  end
143
307
 
144
- def stop_heartbeat_timer
145
- @heartbeat_timer&.cancel
146
- @heartbeat_timer = nil
147
- end
148
308
 
149
- EVENTS.each do |event|
150
- define_method "on_#{event}" do |&block|
151
- @handlers[event.to_sym] = block
152
- end
309
+ protected
153
310
 
154
- define_method "on_#{event}=" do |block|
155
- @handlers[event.to_sym] = block
156
- end
157
- end
311
+ # @note This method is designed to be overridden in subclasses.
312
+ #
313
+ # Returns the full URL of the websocket endpoint to connect to, with path and query parameters
314
+ # if needed. The base implementation simply returns the base URL passed to the initializer.
315
+ #
316
+ # Override this method in subclasses to point to the specific endpoint and add necessary
317
+ # parameters like cursor or filters, depending on the arguments passed to the constructor.
318
+ #
319
+ # @return [String]
158
320
 
159
- def inspectable_variables
160
- instance_variables - [:@handlers, :@ws]
321
+ def build_websocket_url
322
+ @root_url
161
323
  end
162
324
 
163
- def inspect
164
- vars = inspectable_variables.map { |v| "#{v}=#{instance_variable_get(v).inspect}" }.join(", ")
165
- "#<#{self.class}:0x#{object_id} #{vars}>"
325
+ # Builds and configures a websocket client object that is used to connect to the requested service.
326
+ #
327
+ # @return [Faye::WebSocket::Client]
328
+ # see {https://rubydoc.info/gems/faye-websocket/Faye/WebSocket/Client Faye::WebSocket::Client}
329
+
330
+ def build_websocket_client(url)
331
+ Faye::WebSocket::Client.new(url, nil, { headers: { 'User-Agent' => user_agent }.merge(request_headers) })
166
332
  end
167
333
 
334
+ # @note This method is designed to be overridden in subclasses.
335
+ #
336
+ # Processes a single message received from the websocket. The implementation is expected to
337
+ # parse the message from a plain text or binary form, build an appropriate message object,
338
+ # and call the `:message` and/or `:raw_message` callback handlers, passing the right parameters.
339
+ #
340
+ # The base implementation simply takes the message data and passes it as is to `:raw_message`,
341
+ # and does not call `:message` at all.
342
+ #
343
+ # @param msg
344
+ # {https://rubydoc.info/gems/faye-websocket/Faye/WebSocket/API/MessageEvent Faye::WebSocket::API::MessageEvent}
345
+ # @return [nil]
168
346
 
169
- protected
347
+ def handle_message(msg)
348
+ data = msg.data
349
+ @handlers[:raw_message]&.call(data)
350
+ end
351
+
352
+ # Additional headers to pass with the request when connecting to the websocket endpoint.
353
+ # The user agent header (built from {#user_agent}) is added separately.
354
+ #
355
+ # The base implementation returns an empty hash.
356
+ #
357
+ # @return [Hash] a hash of `{ header_name => header_value }`
170
358
 
171
359
  def request_headers
172
360
  {}
173
361
  end
174
362
 
363
+ # Returns the underlying websocket client object. It can be used e.g. to send messages back
364
+ # to the server (but see also: {#send_data}).
365
+ #
366
+ # @return [Faye::WebSocket::Client]
367
+ # see {https://rubydoc.info/gems/faye-websocket/Faye/WebSocket/Client Faye::WebSocket::Client}
368
+
175
369
  def socket
176
370
  @ws
177
371
  end
178
372
 
373
+ # Sends a message back to the server.
374
+ #
375
+ # @param data [String, Array] the message to send -
376
+ # a string for text websockets, a binary string or byte array for binary websockets
377
+ # @return [Boolean] true if the message was sent successfully
378
+
179
379
  def send_data(data)
180
380
  @ws.send(data)
181
381
  end
182
382
 
383
+ # @return [Array<Symbol>] list of instance variables to be printed in the {#inspect} output
384
+ def inspectable_variables
385
+ instance_variables - [:@handlers, :@ws]
386
+ end
387
+
183
388
 
184
389
  private
185
390
 
@@ -187,6 +392,26 @@ module Skyfall
187
392
  EM.reactor_running? && !@engines_on
188
393
  end
189
394
 
395
+ def start_heartbeat_timer
396
+ return if !@check_heartbeat || @heartbeat_interval.to_f <= 0 || @heartbeat_timeout.to_f <= 0
397
+ return if @heartbeat_timer
398
+
399
+ @heartbeat_timer = EM::PeriodicTimer.new(@heartbeat_interval) do
400
+ next if @ws.nil? || @heartbeat_timeout.to_f <= 0
401
+ time_passed = Time.now - @last_update
402
+
403
+ if time_passed > @heartbeat_timeout
404
+ @handlers[:timeout]&.call
405
+ reconnect
406
+ end
407
+ end
408
+ end
409
+
410
+ def stop_heartbeat_timer
411
+ @heartbeat_timer&.cancel
412
+ @heartbeat_timer = nil
413
+ end
414
+
190
415
  def reconnect_delay
191
416
  if @connection_attempts == 0
192
417
  0
@@ -195,14 +420,6 @@ module Skyfall
195
420
  end
196
421
  end
197
422
 
198
- def build_websocket_client(url)
199
- Faye::WebSocket::Client.new(url, nil, { headers: { 'User-Agent' => user_agent }.merge(request_headers) })
200
- end
201
-
202
- def build_websocket_url
203
- @root_url
204
- end
205
-
206
423
  def build_root_url(server)
207
424
  if !server.is_a?(String)
208
425
  raise ArgumentError, "Server parameter should be a string"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Skyfall
4
- VERSION = "0.6.1"
4
+ VERSION = "0.7.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skyfall
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kuba Suder
@@ -10,99 +10,60 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: base32
13
+ name: eventmachine
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.3'
18
+ version: '1.2'
19
19
  - - ">="
20
20
  - !ruby/object:Gem::Version
21
- version: 0.3.4
21
+ version: 1.2.7
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  requirements:
26
26
  - - "~>"
27
27
  - !ruby/object:Gem::Version
28
- version: '0.3'
28
+ version: '1.2'
29
29
  - - ">="
30
30
  - !ruby/object:Gem::Version
31
- version: 0.3.4
32
- - !ruby/object:Gem::Dependency
33
- name: base64
34
- requirement: !ruby/object:Gem::Requirement
35
- requirements:
36
- - - "~>"
37
- - !ruby/object:Gem::Version
38
- version: '0.1'
39
- type: :runtime
40
- prerelease: false
41
- version_requirements: !ruby/object:Gem::Requirement
42
- requirements:
43
- - - "~>"
44
- - !ruby/object:Gem::Version
45
- version: '0.1'
31
+ version: 1.2.7
46
32
  - !ruby/object:Gem::Dependency
47
- name: cbor
33
+ name: faye-websocket
48
34
  requirement: !ruby/object:Gem::Requirement
49
35
  requirements:
50
36
  - - "~>"
51
37
  - !ruby/object:Gem::Version
52
- version: '0.5'
53
- - - ">="
54
- - !ruby/object:Gem::Version
55
- version: 0.5.9.6
38
+ version: '0.12'
56
39
  type: :runtime
57
40
  prerelease: false
58
41
  version_requirements: !ruby/object:Gem::Requirement
59
42
  requirements:
60
43
  - - "~>"
61
44
  - !ruby/object:Gem::Version
62
- version: '0.5'
63
- - - ">="
64
- - !ruby/object:Gem::Version
65
- version: 0.5.9.6
45
+ version: '0.12'
66
46
  - !ruby/object:Gem::Dependency
67
- name: eventmachine
47
+ name: oxygene
68
48
  requirement: !ruby/object:Gem::Requirement
69
49
  requirements:
70
- - - "~>"
71
- - !ruby/object:Gem::Version
72
- version: '1.2'
73
50
  - - ">="
74
51
  - !ruby/object:Gem::Version
75
- version: 1.2.7
52
+ version: 0.0.1
76
53
  type: :runtime
77
54
  prerelease: false
78
55
  version_requirements: !ruby/object:Gem::Requirement
79
56
  requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '1.2'
83
57
  - - ">="
84
58
  - !ruby/object:Gem::Version
85
- version: 1.2.7
86
- - !ruby/object:Gem::Dependency
87
- name: faye-websocket
88
- requirement: !ruby/object:Gem::Requirement
89
- requirements:
90
- - - "~>"
91
- - !ruby/object:Gem::Version
92
- version: '0.12'
93
- type: :runtime
94
- prerelease: false
95
- version_requirements: !ruby/object:Gem::Requirement
96
- requirements:
97
- - - "~>"
98
- - !ruby/object:Gem::Version
99
- version: '0.12'
100
- description: "\n Skyfall is a Ruby library for connecting to the \"firehose\" of
101
- the Bluesky social network, i.e. a websocket which\n streams all new posts and
102
- everything else happening on the Bluesky network in real time. The code connects
103
- to the\n websocket endpoint, decodes the messages which are encoded in some binary
104
- formats, and returns the data as Ruby\n objects, which you can filter and save
105
- to some kind of database (e.g. in order to create a custom feed).\n "
59
+ version: 0.0.1
60
+ description: "\n Skyfall is a Ruby gem for connecting to the \"firehose\" of the
61
+ AT Protocol network that Bluesky is built on,\n i.e. a websocket which streams
62
+ all new posts and everything else happening on the network in real time. The code\n
63
+ \ connects to the websocket endpoint, decodes the messages which are encoded in
64
+ some binary formats, and returns\n the data as Ruby objects and hashes, which
65
+ you can filter and save to some kind of database (e.g. in order\n to create a
66
+ custom feed).\n "
106
67
  email:
107
68
  - jakub.suder@gmail.com
108
69
  executables: []
@@ -113,22 +74,18 @@ files:
113
74
  - LICENSE.txt
114
75
  - README.md
115
76
  - lib/skyfall.rb
116
- - lib/skyfall/car_archive.rb
117
- - lib/skyfall/cid.rb
118
77
  - lib/skyfall/collection.rb
119
78
  - lib/skyfall/errors.rb
120
- - lib/skyfall/extensions.rb
79
+ - lib/skyfall/events.rb
121
80
  - lib/skyfall/firehose.rb
122
81
  - lib/skyfall/firehose/account_message.rb
123
82
  - lib/skyfall/firehose/commit_message.rb
124
- - lib/skyfall/firehose/handle_message.rb
125
83
  - lib/skyfall/firehose/identity_message.rb
126
84
  - lib/skyfall/firehose/info_message.rb
127
85
  - lib/skyfall/firehose/labels_message.rb
128
86
  - lib/skyfall/firehose/message.rb
129
87
  - lib/skyfall/firehose/operation.rb
130
88
  - lib/skyfall/firehose/sync_message.rb
131
- - lib/skyfall/firehose/tombstone_message.rb
132
89
  - lib/skyfall/firehose/unknown_message.rb
133
90
  - lib/skyfall/jetstream.rb
134
91
  - lib/skyfall/jetstream/account_message.rb
@@ -162,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
162
119
  - !ruby/object:Gem::Version
163
120
  version: '0'
164
121
  requirements: []
165
- rubygems_version: 4.0.3
122
+ rubygems_version: 3.6.9
166
123
  specification_version: 4
167
124
  summary: A Ruby gem for streaming data from the Bluesky/ATProto firehose
168
125
  test_files: []
@@ -1,122 +0,0 @@
1
- require_relative 'cid'
2
- require_relative 'errors'
3
- require_relative 'extensions'
4
-
5
- require 'base64'
6
- require 'cbor'
7
- require 'stringio'
8
-
9
- # CAR v1: https://ipld.io/specs/transport/car/carv1/
10
- # multicodec codes: https://github.com/multiformats/multicodec/blob/master/table.csv
11
-
12
- module Skyfall
13
- class CarSection
14
- attr_reader :cid
15
-
16
- def initialize(cid, body_data)
17
- @cid = cid
18
- @body_data = body_data
19
- end
20
-
21
- def body
22
- @body ||= CarArchive.convert_data(CBOR.decode(@body_data))
23
- end
24
- end
25
-
26
- class CarArchive
27
- using Skyfall::Extensions
28
-
29
- attr_reader :roots, :sections
30
-
31
- def initialize(data)
32
- @sections = []
33
-
34
- buffer = StringIO.new(data)
35
- read_header(buffer)
36
- read_section(buffer) until buffer.eof?
37
- end
38
-
39
- def section_with_cid(cid)
40
- section = @sections.detect { |s| s.cid == cid }
41
- section && section.body
42
- end
43
-
44
- def self.convert_data(object)
45
- if object.is_a?(Hash)
46
- object.each do |k, v|
47
- if v.is_a?(Hash) || v.is_a?(Array)
48
- convert_data(v)
49
- elsif v.is_a?(CBOR::Tagged)
50
- object[k] = make_cid_link(v)
51
- elsif v.is_a?(String) && v.encoding == Encoding::ASCII_8BIT
52
- object[k] = make_bytes(v)
53
- end
54
- end
55
- elsif object.is_a?(Array)
56
- object.each_with_index do |v, i|
57
- if v.is_a?(Hash) || v.is_a?(Array)
58
- convert_data(v)
59
- elsif v.is_a?(CBOR::Tagged)
60
- object[i] = make_cid_link(v)
61
- elsif v.is_a?(String) && v.encoding == Encoding::ASCII_8BIT
62
- object[i] = make_bytes(v)
63
- end
64
- end
65
- else
66
- raise DecodeError, "Unexpected value type in record: #{object}"
67
- end
68
- end
69
-
70
- def self.make_cid_link(cid)
71
- { '$link' => CID.from_cbor_tag(cid) }
72
- end
73
-
74
- def self.make_bytes(data)
75
- { '$bytes' => Base64.encode64(data).chomp.gsub(/=+$/, '') }
76
- end
77
-
78
- private
79
-
80
- def read_header(buffer)
81
- len = buffer.read_varint
82
-
83
- header_data = buffer.read(len)
84
- raise DecodeError.new("Header too short: #{header_data}") unless header_data.length == len
85
-
86
- header = CBOR.decode(header_data)
87
- raise UnsupportedError.new("Unexpected CAR version: #{header['version']}") unless header['version'] == 1
88
- @roots = header['roots'].map { |x| CID.from_cbor_tag(x) }
89
- end
90
-
91
- def read_section(buffer)
92
- len = buffer.read_varint
93
-
94
- section_data = buffer.read(len)
95
- raise DecodeError.new("Section too short: #{section_data}") unless section_data.length == len
96
-
97
- sbuffer = StringIO.new(section_data)
98
-
99
- version = sbuffer.read_varint
100
- raise UnsupportedError.new("Unexpected CID version: #{version}") unless version == 1
101
-
102
- codec = sbuffer.read_varint
103
- raise UnsupportedError.new("Unexpected CID codec: #{codec}") unless codec == 0x71 # dag-cbor
104
-
105
- hash = sbuffer.read_varint
106
- raise UnsupportedError.new("Unexpected CID hash: #{hash}") unless hash == 0x12 # sha2-256
107
-
108
- clen = sbuffer.read_varint
109
- raise UnsupportedError.new("Unexpected CID length: #{clen}") unless clen == 32
110
-
111
- prefix = section_data[0...sbuffer.pos]
112
-
113
- cid_data = sbuffer.read(clen)
114
- raise DecodeError.new("CID too short: #{cid_data}") unless cid_data.length == clen
115
-
116
- cid = CID.new(prefix + cid_data)
117
-
118
- body_data = sbuffer.read
119
- @sections << CarSection.new(cid, body_data)
120
- end
121
- end
122
- end