neo4j-core 6.1.6 → 7.0.0.alpha.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
- 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
|