neo4j-core 6.1.6 → 7.0.0.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +4 -9
- data/README.md +48 -0
- data/lib/neo4j-core.rb +23 -0
- data/lib/neo4j-core/helpers.rb +8 -0
- data/lib/neo4j-core/query.rb +23 -20
- data/lib/neo4j-core/query_clauses.rb +18 -32
- data/lib/neo4j-core/query_find_in_batches.rb +3 -1
- data/lib/neo4j-core/version.rb +1 -1
- data/lib/neo4j-embedded/cypher_response.rb +4 -0
- data/lib/neo4j-embedded/embedded_database.rb +3 -5
- data/lib/neo4j-embedded/embedded_node.rb +4 -4
- data/lib/neo4j-embedded/embedded_session.rb +21 -10
- data/lib/neo4j-embedded/embedded_transaction.rb +4 -10
- data/lib/neo4j-server/cypher_node.rb +5 -4
- data/lib/neo4j-server/cypher_relationship.rb +3 -3
- data/lib/neo4j-server/cypher_response.rb +4 -0
- data/lib/neo4j-server/cypher_session.rb +31 -22
- data/lib/neo4j-server/cypher_transaction.rb +23 -15
- data/lib/neo4j-server/resource.rb +3 -4
- data/lib/neo4j/core/cypher_session.rb +17 -9
- data/lib/neo4j/core/cypher_session/adaptors.rb +116 -33
- data/lib/neo4j/core/cypher_session/adaptors/bolt.rb +331 -0
- data/lib/neo4j/core/cypher_session/adaptors/bolt/chunk_writer_io.rb +76 -0
- data/lib/neo4j/core/cypher_session/adaptors/bolt/pack_stream.rb +288 -0
- data/lib/neo4j/core/cypher_session/adaptors/embedded.rb +60 -29
- data/lib/neo4j/core/cypher_session/adaptors/has_uri.rb +63 -0
- data/lib/neo4j/core/cypher_session/adaptors/http.rb +123 -119
- data/lib/neo4j/core/cypher_session/responses.rb +17 -2
- data/lib/neo4j/core/cypher_session/responses/bolt.rb +135 -0
- data/lib/neo4j/core/cypher_session/responses/embedded.rb +46 -11
- data/lib/neo4j/core/cypher_session/responses/http.rb +49 -40
- data/lib/neo4j/core/cypher_session/transactions.rb +33 -0
- data/lib/neo4j/core/cypher_session/transactions/bolt.rb +36 -0
- data/lib/neo4j/core/cypher_session/transactions/embedded.rb +32 -0
- data/lib/neo4j/core/cypher_session/transactions/http.rb +52 -0
- data/lib/neo4j/core/instrumentable.rb +2 -2
- data/lib/neo4j/core/label.rb +182 -0
- data/lib/neo4j/core/node.rb +8 -3
- data/lib/neo4j/core/relationship.rb +12 -4
- data/lib/neo4j/entity_equality.rb +1 -1
- data/lib/neo4j/session.rb +4 -5
- data/lib/neo4j/transaction.rb +108 -72
- data/neo4j-core.gemspec +6 -6
- metadata +34 -40
@@ -0,0 +1,331 @@
|
|
1
|
+
require 'neo4j/core/cypher_session/adaptors'
|
2
|
+
require 'neo4j/core/cypher_session/adaptors/has_uri'
|
3
|
+
require 'neo4j/core/cypher_session/adaptors/bolt/pack_stream'
|
4
|
+
require 'neo4j/core/cypher_session/adaptors/bolt/chunk_writer_io'
|
5
|
+
require 'neo4j/core/cypher_session/responses/bolt'
|
6
|
+
require 'io/wait'
|
7
|
+
|
8
|
+
# TODO: Work with `Query` objects
|
9
|
+
module Neo4j
|
10
|
+
module Core
|
11
|
+
class CypherSession
|
12
|
+
module Adaptors
|
13
|
+
class Bolt < Base
|
14
|
+
include Adaptors::HasUri
|
15
|
+
default_url('bolt://neo4:neo4j@localhost:7687')
|
16
|
+
validate_uri do |uri|
|
17
|
+
uri.scheme == 'bolt'
|
18
|
+
end
|
19
|
+
|
20
|
+
SUPPORTED_VERSIONS = [1, 0, 0, 0].freeze
|
21
|
+
VERSION = '0.0.1'.freeze
|
22
|
+
|
23
|
+
def initialize(url, options = {})
|
24
|
+
self.url = url
|
25
|
+
@options = options
|
26
|
+
|
27
|
+
open_socket
|
28
|
+
end
|
29
|
+
|
30
|
+
def connect
|
31
|
+
handshake
|
32
|
+
|
33
|
+
init
|
34
|
+
|
35
|
+
message = flush_messages[0]
|
36
|
+
return if message.type == :success
|
37
|
+
|
38
|
+
data = message.args[0]
|
39
|
+
fail "Init did not complete successfully\n\n#{data['code']}\n#{data['message']}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def query_set(transaction, queries, options = {})
|
43
|
+
setup_queries!(queries, transaction, skip_instrumentation: options[:skip_instrumentation])
|
44
|
+
|
45
|
+
if @socket.ready?
|
46
|
+
debug_remaining_buffer
|
47
|
+
fail 'Making query, but expected there to be no buffer remaining!'
|
48
|
+
end
|
49
|
+
|
50
|
+
send_query_jobs(queries)
|
51
|
+
|
52
|
+
build_response(queries, options[:wrap_level] || @options[:wrap_level])
|
53
|
+
end
|
54
|
+
|
55
|
+
def version
|
56
|
+
fail 'should be implemented!'
|
57
|
+
end
|
58
|
+
|
59
|
+
def connected?
|
60
|
+
!!@socket
|
61
|
+
end
|
62
|
+
|
63
|
+
def indexes(session)
|
64
|
+
result = query(session, 'CALL db.indexes()', {}, skip_instrumentation: true)
|
65
|
+
|
66
|
+
result.map do |row|
|
67
|
+
label, property = row.description.match(/INDEX ON :([^\(]+)\(([^\)]+)\)/)[1, 2]
|
68
|
+
{type: row.type.to_sym, label: label.to_sym, properties: [property.to_sym]}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def constraints(session)
|
73
|
+
result = query(session, 'CALL db.indexes()', {}, skip_instrumentation: true)
|
74
|
+
|
75
|
+
result.select { |row| row.type == 'node_unique_property' }.map do |row|
|
76
|
+
label, property = row.description.match(/INDEX ON :([^\(]+)\(([^\)]+)\)/)[1, 2]
|
77
|
+
{type: :uniqueness, label: label.to_sym, properties: [property.to_sym]}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.transaction_class
|
82
|
+
require 'neo4j/core/cypher_session/transactions/bolt'
|
83
|
+
Neo4j::Core::CypherSession::Transactions::Bolt
|
84
|
+
end
|
85
|
+
|
86
|
+
instrument(:request, 'neo4j.core.bolt.request', %w(url body)) do |_, start, finish, _id, payload|
|
87
|
+
ms = (finish - start) * 1000
|
88
|
+
|
89
|
+
" #{ANSI::BLUE}BOLT REQUEST:#{ANSI::CLEAR} #{ANSI::YELLOW}#{ms.round}ms#{ANSI::CLEAR} #{payload[:url]}"
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def build_response(queries, wrap_level)
|
95
|
+
catch(:cypher_bolt_failure) do
|
96
|
+
Responses::Bolt.new(queries, method(:flush_messages), wrap_level: wrap_level).results
|
97
|
+
end.tap do |error_data|
|
98
|
+
handle_failure!(error_data) if !error_data.is_a?(Array)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def handle_failure!(error_data)
|
103
|
+
flush_messages
|
104
|
+
|
105
|
+
send_job do |job|
|
106
|
+
job.add_message(:ack_failure)
|
107
|
+
end
|
108
|
+
fail 'Expected SUCCESS for ACK_FAILURE' if flush_messages[0].type != :success
|
109
|
+
|
110
|
+
fail CypherError.new_from(error_data['code'], error_data['message'])
|
111
|
+
end
|
112
|
+
|
113
|
+
def debug_remaining_buffer
|
114
|
+
logger.debug 'Remaining buffer:'
|
115
|
+
|
116
|
+
i = 0
|
117
|
+
while @socket.ready?
|
118
|
+
i += 1
|
119
|
+
logger.debug "Message set #{i}:"
|
120
|
+
flush_messages
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def send_query_jobs(queries)
|
125
|
+
send_job do |job|
|
126
|
+
queries.each do |query|
|
127
|
+
job.add_message(:run, query.cypher, query.parameters || {})
|
128
|
+
job.add_message(:pull_all)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def new_job
|
134
|
+
Job.new(self)
|
135
|
+
end
|
136
|
+
|
137
|
+
def open_socket
|
138
|
+
@socket = TCPSocket.open(host, port)
|
139
|
+
end
|
140
|
+
|
141
|
+
GOGOBOLT = "\x60\x60\xB0\x17"
|
142
|
+
def handshake
|
143
|
+
log_message :C, :handshake, nil
|
144
|
+
|
145
|
+
sendmsg(GOGOBOLT + SUPPORTED_VERSIONS.pack('l>*'))
|
146
|
+
|
147
|
+
agreed_version = recvmsg(4).unpack('l>*')[0]
|
148
|
+
|
149
|
+
if agreed_version.zero?
|
150
|
+
@socket.shutdown(Socket::SHUT_RDWR)
|
151
|
+
@socket.close
|
152
|
+
|
153
|
+
fail "Couldn't agree on a version (Sent versions #{SUPPORTED_VERSIONS.inspect})"
|
154
|
+
end
|
155
|
+
|
156
|
+
logger.debug "Agreed to version: #{agreed_version}"
|
157
|
+
end
|
158
|
+
|
159
|
+
def init
|
160
|
+
send_job do |job|
|
161
|
+
job.add_message(:init, USER_AGENT_STRING, principal: user, credentials: password, scheme: 'basic')
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Allows you to send messages to the server
|
166
|
+
# Returns an array of Message objects
|
167
|
+
def send_job
|
168
|
+
new_job.tap do |job|
|
169
|
+
yield job
|
170
|
+
log_message :C, :job, job
|
171
|
+
sendmsg(job.chunked_packed_stream)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
STREAM_INSPECTOR = lambda do |stream|
|
176
|
+
stream.bytes.map { |byte| byte.to_s(16).rjust(2, '0') }.join(':')
|
177
|
+
end
|
178
|
+
|
179
|
+
def sendmsg(message)
|
180
|
+
log_message :C, message
|
181
|
+
|
182
|
+
@socket.sendmsg(message)
|
183
|
+
end
|
184
|
+
|
185
|
+
def recvmsg(size, timeout = 10)
|
186
|
+
Timeout.timeout(timeout) do
|
187
|
+
@socket.recv(size).tap do |result|
|
188
|
+
log_message :S, result
|
189
|
+
end
|
190
|
+
end
|
191
|
+
rescue Timeout::Error
|
192
|
+
raise "Timed out waiting for #{size} bytes from Neo4j (after #{timeout} seconds)"
|
193
|
+
end
|
194
|
+
|
195
|
+
def flush_messages
|
196
|
+
if structures = flush_response
|
197
|
+
structures.map do |structure|
|
198
|
+
Message.new(structure.signature, *structure.list).tap do |message|
|
199
|
+
log_message :S, message.type, message.args.join(' ')
|
200
|
+
end
|
201
|
+
end.tap { flush_response }
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def log_message(side, *args)
|
206
|
+
if args.size == 1
|
207
|
+
logger.debug "#{side}: #{STREAM_INSPECTOR.call(args[0])}"
|
208
|
+
else
|
209
|
+
type, message = args
|
210
|
+
logger.debug "#{side}: #{ANSI::CYAN}#{type.to_s.upcase}#{ANSI::CLEAR} #{message}"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Replace with Enumerator?
|
215
|
+
def flush_response
|
216
|
+
if !(header = recvmsg(2)).empty? && (chunk_size = header.unpack('s>*')[0]) > 0
|
217
|
+
log_message :S, :chunk_size, chunk_size
|
218
|
+
|
219
|
+
chunk = recvmsg(chunk_size)
|
220
|
+
|
221
|
+
unpacker = PackStream::Unpacker.new(StringIO.new(chunk))
|
222
|
+
|
223
|
+
[].tap { |r| while arg = unpacker.unpack_value!; r << arg; end }
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Represents messages sent to or received from the server
|
228
|
+
class Message
|
229
|
+
TYPE_CODES = {
|
230
|
+
# client message types
|
231
|
+
init: 0x01, # 0000 0001 // INIT <user_agent>
|
232
|
+
ack_failure: 0x0E, # 0000 1110 // ACK_FAILURE
|
233
|
+
reset: 0x0F, # 0000 1111 // RESET
|
234
|
+
run: 0x10, # 0001 0000 // RUN <statement> <parameters>
|
235
|
+
discard_all: 0x2F, # 0010 1111 // DISCARD *
|
236
|
+
pull_all: 0x3F, # 0011 1111 // PULL *
|
237
|
+
|
238
|
+
# server message types
|
239
|
+
success: 0x70, # 0111 0000 // SUCCESS <metadata>
|
240
|
+
record: 0x71, # 0111 0001 // RECORD <value>
|
241
|
+
ignored: 0x7E, # 0111 1110 // IGNORED <metadata>
|
242
|
+
failure: 0x7F # 0111 1111 // FAILURE <metadata>
|
243
|
+
}.freeze
|
244
|
+
|
245
|
+
CODE_TYPES = TYPE_CODES.invert
|
246
|
+
|
247
|
+
def initialize(type_or_code, *args)
|
248
|
+
@type_code = Message.type_code_for(type_or_code)
|
249
|
+
fail "Invalid message type: #{@type_code.inspect}" if !@type_code
|
250
|
+
@type = CODE_TYPES[@type_code]
|
251
|
+
|
252
|
+
@args = args
|
253
|
+
end
|
254
|
+
|
255
|
+
def struct
|
256
|
+
PackStream::Structure.new(@type_code, @args)
|
257
|
+
end
|
258
|
+
|
259
|
+
def to_s
|
260
|
+
"#{ANSI::GREEN}#{@type.to_s.upcase}#{ANSI::CLEAR} #{@args.inspect if !@args.empty?}"
|
261
|
+
end
|
262
|
+
|
263
|
+
def packed_stream
|
264
|
+
PackStream::Packer.new(struct).packed_stream
|
265
|
+
end
|
266
|
+
|
267
|
+
def value
|
268
|
+
return if @type != :record
|
269
|
+
@args.map do |arg|
|
270
|
+
# Assuming result is Record
|
271
|
+
field_names = arg[1]
|
272
|
+
|
273
|
+
field_values = arg[2].map do |field_value|
|
274
|
+
Message.interpret_field_value(field_value)
|
275
|
+
end
|
276
|
+
|
277
|
+
Hash[field_names.zip(field_values)]
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
attr_reader :type, :args
|
282
|
+
|
283
|
+
def self.type_code_for(type_or_code)
|
284
|
+
type_or_code.is_a?(Integer) ? type_or_code : TYPE_CODES[type_or_code]
|
285
|
+
end
|
286
|
+
|
287
|
+
def self.interpret_field_value(value)
|
288
|
+
if value.is_a?(Array) && (1..3).cover?(value[0])
|
289
|
+
case value[0]
|
290
|
+
when 1
|
291
|
+
{type: :node, identity: value[1],
|
292
|
+
labels: value[2], properties: value[3]}
|
293
|
+
end
|
294
|
+
else
|
295
|
+
value
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# Represents a set of messages to send to the server
|
301
|
+
class Job
|
302
|
+
def initialize(session)
|
303
|
+
@messages = []
|
304
|
+
@session = session
|
305
|
+
end
|
306
|
+
|
307
|
+
def add_message(type, *args)
|
308
|
+
@messages << Message.new(type, *args)
|
309
|
+
end
|
310
|
+
|
311
|
+
def chunked_packed_stream
|
312
|
+
io = ChunkWriterIO.new
|
313
|
+
|
314
|
+
@messages.each do |message|
|
315
|
+
io.write(message.packed_stream)
|
316
|
+
io.flush(true)
|
317
|
+
end
|
318
|
+
|
319
|
+
io.rewind
|
320
|
+
io.read
|
321
|
+
end
|
322
|
+
|
323
|
+
def to_s
|
324
|
+
@messages.join(' | ')
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
class ChunkWriterIO < StringIO
|
4
|
+
# Writer for chunked data.
|
5
|
+
|
6
|
+
MAX_CHUNK_SIZE = 0xFFFF
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@output_buffer = []
|
10
|
+
@output_size = 0
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
# Write some bytes, splitting into chunks if necessary.
|
15
|
+
def write_with_chunking(string)
|
16
|
+
# Kernel.puts "Write!"
|
17
|
+
until string.empty?
|
18
|
+
future_size = @output_size + string.size
|
19
|
+
if future_size >= MAX_CHUNK_SIZE
|
20
|
+
last = MAX_CHUNK_SIZE - @output_size
|
21
|
+
write_buffer!(string[0, last], MAX_CHUNK_SIZE)
|
22
|
+
string = string[last..-1]
|
23
|
+
|
24
|
+
write_without_chunking(buffer_result)
|
25
|
+
clear_buffer!
|
26
|
+
else
|
27
|
+
write_buffer!(string, future_size)
|
28
|
+
|
29
|
+
string = ''
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
alias write_without_chunking write
|
35
|
+
alias write write_with_chunking
|
36
|
+
|
37
|
+
def flush(zero_chunk = false)
|
38
|
+
write_without_chunking(buffer_result(zero_chunk))
|
39
|
+
clear_buffer!
|
40
|
+
|
41
|
+
super()
|
42
|
+
end
|
43
|
+
|
44
|
+
# Close the stream.
|
45
|
+
def close(zero_chunk = false)
|
46
|
+
flush(zero_chunk)
|
47
|
+
super
|
48
|
+
end
|
49
|
+
|
50
|
+
# private
|
51
|
+
def write_buffer!(string, size)
|
52
|
+
@output_buffer << string
|
53
|
+
@output_size = size
|
54
|
+
end
|
55
|
+
|
56
|
+
def buffer_result(zero_chunk = false)
|
57
|
+
result = ''
|
58
|
+
|
59
|
+
# Kernel.puts 'result1', result.inspect
|
60
|
+
if !@output_buffer.empty?
|
61
|
+
result << [@output_size].pack('s>*')
|
62
|
+
result.concat(@output_buffer.join)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Kernel.puts 'result2', result.inspect
|
66
|
+
result << "\x00\x00" if zero_chunk
|
67
|
+
# Kernel.puts 'result3', result.inspect
|
68
|
+
|
69
|
+
result
|
70
|
+
end
|
71
|
+
|
72
|
+
def clear_buffer!
|
73
|
+
@output_buffer.clear
|
74
|
+
@output_size = 0
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,288 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
module Neo4j
|
4
|
+
module Core
|
5
|
+
# Implements the the PackStream packing and unpacking specifications
|
6
|
+
# as specified by Neo Technology for the Neo4j graph database
|
7
|
+
module PackStream
|
8
|
+
MARKER_TYPES = {
|
9
|
+
C0: nil,
|
10
|
+
C1: [:float, 64],
|
11
|
+
C2: false,
|
12
|
+
C3: true,
|
13
|
+
C8: [:int, 8],
|
14
|
+
C9: [:int, 16],
|
15
|
+
CA: [:int, 32],
|
16
|
+
CB: [:int, 64],
|
17
|
+
CC: [:bytes, 8],
|
18
|
+
CD: [:bytes, 16],
|
19
|
+
CE: [:bytes, 32],
|
20
|
+
D0: [:text, 8],
|
21
|
+
D1: [:text, 16],
|
22
|
+
D2: [:text, 32],
|
23
|
+
D4: [:list, 8],
|
24
|
+
D5: [:list, 16],
|
25
|
+
D6: [:list, 32],
|
26
|
+
D8: [:map, 8],
|
27
|
+
D9: [:map, 16],
|
28
|
+
DA: [:map, 32],
|
29
|
+
DC: [:struct, 8],
|
30
|
+
DD: [:struct, 16],
|
31
|
+
DE: [:struct, 32]
|
32
|
+
}
|
33
|
+
# For efficiency. Translates directly from bytes to types
|
34
|
+
MARKER_TYPES.keys.each do |key|
|
35
|
+
ord = eval("0x#{key}") # rubocop:disable Lint/Eval
|
36
|
+
MARKER_TYPES[ord] = MARKER_TYPES.delete(key)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Translates directly from types to bytes
|
40
|
+
MARKER_BYTES = MARKER_TYPES.invert
|
41
|
+
MARKER_BYTES.keys.each do |key|
|
42
|
+
MARKER_BYTES.delete(key) if key.is_a?(Array)
|
43
|
+
end
|
44
|
+
|
45
|
+
MARKER_HEADERS = MARKER_TYPES.each_with_object({}) do |(byte, (type, size)), headers|
|
46
|
+
headers[type] ||= {}
|
47
|
+
headers[type][size] = [byte].pack('C')
|
48
|
+
end
|
49
|
+
|
50
|
+
HEADER_PACK_STRINGS = %w(C S L).freeze
|
51
|
+
|
52
|
+
Structure = Struct.new(:signature, :list)
|
53
|
+
|
54
|
+
# Object which holds a Ruby object and can
|
55
|
+
# pack it into a PackStream stream
|
56
|
+
class Packer
|
57
|
+
def initialize(object)
|
58
|
+
@object = object
|
59
|
+
end
|
60
|
+
|
61
|
+
def packed_stream
|
62
|
+
if byte = MARKER_BYTES[@object]
|
63
|
+
pack_array_as_string([byte])
|
64
|
+
else
|
65
|
+
case @object
|
66
|
+
when Date, Time, DateTime then string_stream
|
67
|
+
when Integer, Float, String, Symbol, Array, Structure, Hash
|
68
|
+
send(@object.class.name.split('::').last.downcase + '_stream')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Range Minimum | Range Maximum | Representation | Byte |
|
74
|
+
# ============================|============================|================|======|
|
75
|
+
# -9 223 372 036 854 775 808 | -2 147 483 649 | INT_64 | CB |
|
76
|
+
# -2 147 483 648 | -32 769 | INT_32 | CA |
|
77
|
+
# -32 768 | -129 | INT_16 | C9 |
|
78
|
+
# -128 | -17 | INT_8 | C8 |
|
79
|
+
# -16 | +127 | TINY_INT | N/A |
|
80
|
+
# +128 | +32 767 | INT_16 | C9 |
|
81
|
+
# +32 768 | +2 147 483 647 | INT_32 | CA |
|
82
|
+
# +2 147 483 648 | +9 223 372 036 854 775 807 | INT_64 | CB |
|
83
|
+
|
84
|
+
INT_HEADERS = MARKER_HEADERS[:int]
|
85
|
+
def integer_stream
|
86
|
+
case @object
|
87
|
+
when -0x10...0x80 # TINY_INT
|
88
|
+
pack_integer_object_as_string
|
89
|
+
when -0x80...-0x10 # INT_8
|
90
|
+
INT_HEADERS[8] + pack_integer_object_as_string
|
91
|
+
when -0x8000...0x8000 # INT_16
|
92
|
+
INT_HEADERS[16] + pack_integer_object_as_string(2)
|
93
|
+
when -0x80000000...0x80000000 # INT_32
|
94
|
+
INT_HEADERS[32] + pack_integer_object_as_string(4)
|
95
|
+
when -0x8000000000000000...0x8000000000000000 # INT_64
|
96
|
+
INT_HEADERS[64] + pack_integer_object_as_string(8)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
alias fixnum_stream integer_stream
|
101
|
+
alias bignum_stream integer_stream
|
102
|
+
|
103
|
+
def float_stream
|
104
|
+
MARKER_HEADERS[:float][64] + [@object].pack('G').force_encoding(Encoding::BINARY)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Marker | Size | Maximum size
|
108
|
+
# ========|=============================================|=====================
|
109
|
+
# 80..8F | contained within low-order nibble of marker | 15 bytes
|
110
|
+
# D0 | 8-bit big-endian unsigned integer | 255 bytes
|
111
|
+
# D1 | 16-bit big-endian unsigned integer | 65 535 bytes
|
112
|
+
# D2 | 32-bit big-endian unsigned integer | 4 294 967 295 bytes
|
113
|
+
|
114
|
+
def string_stream
|
115
|
+
s = @object.to_s
|
116
|
+
s = s.dup if s.frozen?
|
117
|
+
marker_string(0x80, 0xD0, @object.to_s.bytesize) + s.force_encoding(Encoding::BINARY)
|
118
|
+
end
|
119
|
+
|
120
|
+
alias symbol_stream string_stream
|
121
|
+
|
122
|
+
def array_stream
|
123
|
+
marker_string(0x90, 0xD4, @object.size) + @object.map do |e|
|
124
|
+
Packer.new(e).packed_stream
|
125
|
+
end.join
|
126
|
+
end
|
127
|
+
|
128
|
+
def structure_stream
|
129
|
+
fail 'Structure too big' if @object.list.size > 65_535
|
130
|
+
marker_string(0xB0, 0xDC, @object.list.size) + [@object.signature].pack('C') + @object.list.map do |e|
|
131
|
+
Packer.new(e).packed_stream
|
132
|
+
end.join
|
133
|
+
end
|
134
|
+
|
135
|
+
def hash_stream
|
136
|
+
marker_string(0xA0, 0xD8, @object.size) +
|
137
|
+
@object.map do |key, value|
|
138
|
+
Packer.new(key).packed_stream +
|
139
|
+
Packer.new(value).packed_stream
|
140
|
+
end.join
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.pack_arguments(*objects)
|
144
|
+
objects.map { |o| new(o).packed_stream }.join
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def marker_string(tiny_base, regular_base, size)
|
150
|
+
head_byte = case size
|
151
|
+
when 0...0x10 then tiny_base + size
|
152
|
+
when 0x10...0x100 then regular_base
|
153
|
+
when 0x100...0x10000 then regular_base + 1
|
154
|
+
when 0x10000...0x100000000 then regular_base + 2
|
155
|
+
end
|
156
|
+
|
157
|
+
result = [head_byte].pack('C')
|
158
|
+
result += [size].pack(HEADER_PACK_STRINGS[head_byte - regular_base]).reverse if size >= 0x10
|
159
|
+
result
|
160
|
+
end
|
161
|
+
|
162
|
+
def pack_integer_object_as_string(size = 1)
|
163
|
+
bytes = []
|
164
|
+
(0...size).to_a.reverse.inject(@object) do |current, i|
|
165
|
+
bytes << (current / (256**i))
|
166
|
+
current % (256**i)
|
167
|
+
end
|
168
|
+
|
169
|
+
pack_array_as_string(bytes)
|
170
|
+
end
|
171
|
+
|
172
|
+
def pack_array_as_string(a)
|
173
|
+
a.pack('c*')
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Object which holds a stream of PackStream data
|
178
|
+
# and can unpack it
|
179
|
+
class Unpacker
|
180
|
+
def initialize(stream)
|
181
|
+
@stream = stream
|
182
|
+
end
|
183
|
+
|
184
|
+
HEADER_BASE_BYTES = {text: 0xD0, list: 0xD4, struct: 0xDC, map: 0xD8}.freeze
|
185
|
+
|
186
|
+
def unpack_value!
|
187
|
+
return nil if depleted?
|
188
|
+
|
189
|
+
marker = shift_byte!
|
190
|
+
|
191
|
+
if type_and_size = PackStream.marker_type_and_size(marker)
|
192
|
+
type, size = type_and_size
|
193
|
+
|
194
|
+
shift_value_for_type!(type, size, marker)
|
195
|
+
elsif MARKER_TYPES.key?(marker)
|
196
|
+
MARKER_TYPES[marker]
|
197
|
+
else
|
198
|
+
marker >= 0xF0 ? -0x100 + marker : marker
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
private
|
203
|
+
|
204
|
+
METHOD_MAP = {
|
205
|
+
int: :value_for_int!,
|
206
|
+
float: :value_for_float!,
|
207
|
+
tiny_list: :value_for_list!,
|
208
|
+
list: :value_for_list!,
|
209
|
+
tiny_map: :value_for_map!,
|
210
|
+
map: :value_for_map!,
|
211
|
+
tiny_struct: :value_for_struct!,
|
212
|
+
struct: :value_for_struct!
|
213
|
+
}
|
214
|
+
|
215
|
+
def shift_value_for_type!(type, size, marker)
|
216
|
+
if [:text, :list, :map, :struct].include?(type)
|
217
|
+
offset = marker - HEADER_BASE_BYTES[type]
|
218
|
+
size = shift_stream!(2 << (offset - 1)).reverse.unpack(HEADER_PACK_STRINGS[offset])[0]
|
219
|
+
end
|
220
|
+
|
221
|
+
if [:tiny_text, :text, :bytes].include?(type)
|
222
|
+
shift_stream!(size).force_encoding('UTF-8')
|
223
|
+
else
|
224
|
+
send(METHOD_MAP[type], size)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def value_for_int!(size)
|
229
|
+
r = shift_bytes!(size >> 3).reverse.each_with_index.inject(0) do |sum, (byte, i)|
|
230
|
+
sum + (byte * (256**i))
|
231
|
+
end
|
232
|
+
|
233
|
+
(r >> (size - 1)) == 1 ? (r - (2**size)) : r
|
234
|
+
end
|
235
|
+
|
236
|
+
def value_for_float!(_size)
|
237
|
+
shift_stream!(8).unpack('G')[0]
|
238
|
+
end
|
239
|
+
|
240
|
+
def value_for_map!(size)
|
241
|
+
size.times.each_with_object({}) do |_, r|
|
242
|
+
key = unpack_value!
|
243
|
+
r[key] = unpack_value!
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def value_for_list!(size)
|
248
|
+
Array.new(size) { unpack_value! }
|
249
|
+
end
|
250
|
+
|
251
|
+
def value_for_struct!(size)
|
252
|
+
Structure.new(shift_byte!, value_for_list!(size))
|
253
|
+
end
|
254
|
+
|
255
|
+
|
256
|
+
def shift_byte!
|
257
|
+
shift_bytes!(1).first unless depleted?
|
258
|
+
end
|
259
|
+
|
260
|
+
def shift_bytes!(length)
|
261
|
+
result = shift_stream!(length)
|
262
|
+
result && result.bytes.to_a
|
263
|
+
end
|
264
|
+
|
265
|
+
def shift_stream!(length)
|
266
|
+
@stream.read(length) if !depleted? || length.zero?
|
267
|
+
end
|
268
|
+
|
269
|
+
def depleted?
|
270
|
+
@stream.eof?
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def self.marker_type_and_size(marker)
|
275
|
+
if (marker_spec = MARKER_TYPES[marker]).is_a?(Array)
|
276
|
+
marker_spec
|
277
|
+
else
|
278
|
+
case marker
|
279
|
+
when 0x80..0x8F then [:tiny_text, marker - 0x80]
|
280
|
+
when 0x90..0x9F then [:tiny_list, marker - 0x90]
|
281
|
+
when 0xA0..0xAF then [:tiny_map, marker - 0xA0]
|
282
|
+
when 0xB0..0xBF then [:tiny_struct, marker - 0xB0]
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|