outback 0.0.14 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +6 -0
  3. data/LICENSE +21 -0
  4. data/README.md +29 -3
  5. data/lib/outback.rb +16 -11
  6. data/lib/outback/archive.rb +6 -17
  7. data/lib/outback/backup.rb +24 -19
  8. data/lib/outback/cli.rb +6 -2
  9. data/lib/outback/configuration.rb +15 -10
  10. data/lib/outback/directory_source.rb +8 -7
  11. data/lib/outback/directory_target.rb +18 -11
  12. data/lib/outback/encryption_processor.rb +34 -0
  13. data/lib/outback/errors.rb +7 -0
  14. data/lib/outback/logging.rb +7 -0
  15. data/lib/outback/mysql_source.rb +9 -9
  16. data/lib/outback/processor.rb +17 -0
  17. data/lib/outback/s3_target.rb +18 -9
  18. data/lib/outback/sftp_target.rb +70 -0
  19. data/lib/outback/source.rb +9 -2
  20. data/lib/outback/source_archive.rb +17 -0
  21. data/lib/outback/support/attr_setter.rb +1 -1
  22. data/lib/outback/support/configurable.rb +5 -3
  23. data/lib/outback/target.rb +51 -14
  24. data/lib/outback/target_archive.rb +30 -0
  25. data/lib/outback/version.rb +3 -0
  26. data/lib/vendor/enumerable_ext.rb +9 -0
  27. data/lib/{outback/vendor → vendor}/metaclass.rb +1 -1
  28. data/lib/{outback/vendor → vendor}/methodphitamine.rb +1 -1
  29. data/lib/vendor/mysql.rb +1093 -0
  30. data/lib/vendor/mysql/charset.rb +325 -0
  31. data/lib/vendor/mysql/constants.rb +165 -0
  32. data/lib/vendor/mysql/error.rb +989 -0
  33. data/lib/vendor/mysql/packet.rb +78 -0
  34. data/lib/vendor/mysql/protocol.rb +770 -0
  35. data/lib/vendor/numeric_ext.rb +49 -0
  36. data/lib/vendor/string_ext.rb +19 -0
  37. metadata +53 -39
  38. data/MIT-LICENSE +0 -20
  39. data/VERSION +0 -1
  40. data/lib/outback/configuration_error.rb +0 -4
  41. data/lib/outback/directory_archive.rb +0 -8
  42. data/lib/outback/local_archive.rb +0 -6
  43. data/lib/outback/s3_archive.rb +0 -18
  44. data/lib/outback/temp_archive.rb +0 -5
  45. data/lib/outback/vendor/mysql.rb +0 -1214
@@ -0,0 +1,78 @@
1
+ # coding: ascii-8bit
2
+ class Mysql
3
+ class Packet
4
+ # convert Numeric to LengthCodedBinary
5
+ def self.lcb(num)
6
+ return "\xfb" if num.nil?
7
+ return [num].pack("C") if num < 251
8
+ return [252, num].pack("Cv") if num < 65536
9
+ return [253, num&0xffff, num>>16].pack("CvC") if num < 16777216
10
+ return [254, num&0xffffffff, num>>32].pack("CVV")
11
+ end
12
+
13
+ # convert String to LengthCodedString
14
+ def self.lcs(str)
15
+ str = Charset.to_binary str.dup
16
+ lcb(str.length)+str
17
+ end
18
+
19
+ def initialize(data)
20
+ @data = data
21
+ end
22
+
23
+ def lcb
24
+ return nil if @data.empty?
25
+ case v = utiny
26
+ when 0xfb
27
+ return nil
28
+ when 0xfc
29
+ return ushort
30
+ when 0xfd
31
+ c, v = utiny, ushort
32
+ return (v << 8)+c
33
+ when 0xfe
34
+ v1, v2 = ulong, ulong
35
+ return (v2 << 32)+v1
36
+ else
37
+ return v
38
+ end
39
+ end
40
+
41
+ def lcs
42
+ len = self.lcb
43
+ return nil unless len
44
+ @data.slice!(0, len)
45
+ end
46
+
47
+ def read(len)
48
+ @data.slice!(0, len)
49
+ end
50
+
51
+ def string
52
+ str = @data.unpack('Z*').first
53
+ @data.slice!(0, str.length+1)
54
+ str
55
+ end
56
+
57
+ def utiny
58
+ @data.slice!(0, 1).unpack('C').first
59
+ end
60
+
61
+ def ushort
62
+ @data.slice!(0, 2).unpack('v').first
63
+ end
64
+
65
+ def ulong
66
+ @data.slice!(0, 4).unpack('V').first
67
+ end
68
+
69
+ def eof?
70
+ @data[0] == ?\xfe && @data.length == 5
71
+ end
72
+
73
+ def to_s
74
+ @data
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,770 @@
1
+ # coding: ascii-8bit
2
+ # Copyright (C) 2008-2012 TOMITA Masahiro
3
+ # mailto:tommy@tmtm.org
4
+
5
+ require "socket"
6
+ require "timeout"
7
+ require "digest/sha1"
8
+ require "stringio"
9
+
10
+ class Mysql
11
+ # MySQL network protocol
12
+ class Protocol
13
+
14
+ VERSION = 10
15
+ MAX_PACKET_LENGTH = 2**24-1
16
+
17
+ # Convert netdata to Ruby value
18
+ # === Argument
19
+ # data :: [Packet] packet data
20
+ # type :: [Integer] field type
21
+ # unsigned :: [true or false] true if value is unsigned
22
+ # === Return
23
+ # Object :: converted value.
24
+ def self.net2value(pkt, type, unsigned)
25
+ case type
26
+ when Field::TYPE_STRING, Field::TYPE_VAR_STRING, Field::TYPE_NEWDECIMAL, Field::TYPE_BLOB
27
+ return pkt.lcs
28
+ when Field::TYPE_TINY
29
+ v = pkt.utiny
30
+ return unsigned ? v : v < 128 ? v : v-256
31
+ when Field::TYPE_SHORT
32
+ v = pkt.ushort
33
+ return unsigned ? v : v < 32768 ? v : v-65536
34
+ when Field::TYPE_INT24, Field::TYPE_LONG
35
+ v = pkt.ulong
36
+ return unsigned ? v : v < 0x8000_0000 ? v : v-0x10000_0000
37
+ when Field::TYPE_LONGLONG
38
+ n1, n2 = pkt.ulong, pkt.ulong
39
+ v = (n2 << 32) | n1
40
+ return unsigned ? v : v < 0x8000_0000_0000_0000 ? v : v-0x10000_0000_0000_0000
41
+ when Field::TYPE_FLOAT
42
+ return pkt.read(4).unpack('e').first
43
+ when Field::TYPE_DOUBLE
44
+ return pkt.read(8).unpack('E').first
45
+ when Field::TYPE_DATE
46
+ len = pkt.utiny
47
+ y, m, d = pkt.read(len).unpack("vCC")
48
+ t = Mysql::Time.new(y, m, d, nil, nil, nil)
49
+ return t
50
+ when Field::TYPE_DATETIME, Field::TYPE_TIMESTAMP
51
+ len = pkt.utiny
52
+ y, m, d, h, mi, s, sp = pkt.read(len).unpack("vCCCCCV")
53
+ return Mysql::Time.new(y, m, d, h, mi, s, false, sp)
54
+ when Field::TYPE_TIME
55
+ len = pkt.utiny
56
+ sign, d, h, mi, s, sp = pkt.read(len).unpack("CVCCCV")
57
+ h = d.to_i * 24 + h.to_i
58
+ return Mysql::Time.new(0, 0, 0, h, mi, s, sign!=0, sp)
59
+ when Field::TYPE_YEAR
60
+ return pkt.ushort
61
+ when Field::TYPE_BIT
62
+ return pkt.lcs
63
+ else
64
+ raise "not implemented: type=#{type}"
65
+ end
66
+ end
67
+
68
+ # convert Ruby value to netdata
69
+ # === Argument
70
+ # v :: [Object] Ruby value.
71
+ # === Return
72
+ # Integer :: type of column. Field::TYPE_*
73
+ # String :: netdata
74
+ # === Exception
75
+ # ProtocolError :: value too large / value is not supported
76
+ def self.value2net(v)
77
+ case v
78
+ when nil
79
+ type = Field::TYPE_NULL
80
+ val = ""
81
+ when Integer
82
+ if -0x8000_0000 <= v && v < 0x8000_0000
83
+ type = Field::TYPE_LONG
84
+ val = [v].pack('V')
85
+ elsif -0x8000_0000_0000_0000 <= v && v < 0x8000_0000_0000_0000
86
+ type = Field::TYPE_LONGLONG
87
+ val = [v&0xffffffff, v>>32].pack("VV")
88
+ elsif 0x8000_0000_0000_0000 <= v && v <= 0xffff_ffff_ffff_ffff
89
+ type = Field::TYPE_LONGLONG | 0x8000
90
+ val = [v&0xffffffff, v>>32].pack("VV")
91
+ else
92
+ raise ProtocolError, "value too large: #{v}"
93
+ end
94
+ when Float
95
+ type = Field::TYPE_DOUBLE
96
+ val = [v].pack("E")
97
+ when String
98
+ type = Field::TYPE_STRING
99
+ val = Packet.lcs(v)
100
+ when ::Time
101
+ type = Field::TYPE_DATETIME
102
+ val = [11, v.year, v.month, v.day, v.hour, v.min, v.sec, v.usec].pack("CvCCCCCV")
103
+ when Mysql::Time
104
+ type = Field::TYPE_DATETIME
105
+ val = [11, v.year, v.month, v.day, v.hour, v.min, v.sec, v.second_part].pack("CvCCCCCV")
106
+ else
107
+ raise ProtocolError, "class #{v.class} is not supported"
108
+ end
109
+ return type, val
110
+ end
111
+
112
+ attr_reader :server_info
113
+ attr_reader :server_version
114
+ attr_reader :thread_id
115
+ attr_reader :sqlstate
116
+ attr_reader :affected_rows
117
+ attr_reader :insert_id
118
+ attr_reader :server_status
119
+ attr_reader :warning_count
120
+ attr_reader :message
121
+ attr_accessor :charset
122
+
123
+ # @state variable keep state for connection.
124
+ # :INIT :: Initial state.
125
+ # :READY :: Ready for command.
126
+ # :FIELD :: After query(). retr_fields() is needed.
127
+ # :RESULT :: After retr_fields(), retr_all_records() or stmt_retr_all_records() is needed.
128
+
129
+ # make socket connection to server.
130
+ # === Argument
131
+ # host :: [String] if "localhost" or "" nil then use UNIXSocket. Otherwise use TCPSocket
132
+ # port :: [Integer] port number using by TCPSocket
133
+ # socket :: [String] socket file name using by UNIXSocket
134
+ # conn_timeout :: [Integer] connect timeout (sec).
135
+ # read_timeout :: [Integer] read timeout (sec).
136
+ # write_timeout :: [Integer] write timeout (sec).
137
+ # === Exception
138
+ # [ClientError] :: connection timeout
139
+ def initialize(host, port, socket, conn_timeout, read_timeout, write_timeout)
140
+ @insert_id = 0
141
+ @warning_count = 0
142
+ @gc_stmt_queue = [] # stmt id list which GC destroy.
143
+ set_state :INIT
144
+ @read_timeout = read_timeout
145
+ @write_timeout = write_timeout
146
+ begin
147
+ Timeout.timeout conn_timeout do
148
+ if host.nil? or host.empty? or host == "localhost"
149
+ socket ||= ENV["MYSQL_UNIX_PORT"] || MYSQL_UNIX_PORT
150
+ @sock = UNIXSocket.new socket
151
+ else
152
+ port ||= ENV["MYSQL_TCP_PORT"] || (Socket.getservbyname("mysql","tcp") rescue MYSQL_TCP_PORT)
153
+ @sock = TCPSocket.new host, port
154
+ end
155
+ end
156
+ rescue Timeout::Error
157
+ raise ClientError, "connection timeout"
158
+ end
159
+ end
160
+
161
+ def close
162
+ @sock.close
163
+ end
164
+
165
+ # initial negotiate and authenticate.
166
+ # === Argument
167
+ # user :: [String / nil] username
168
+ # passwd :: [String / nil] password
169
+ # db :: [String / nil] default database name. nil: no default.
170
+ # flag :: [Integer] client flag
171
+ # charset :: [Mysql::Charset / nil] charset for connection. nil: use server's charset
172
+ # === Exception
173
+ # ProtocolError :: The old style password is not supported
174
+ def authenticate(user, passwd, db, flag, charset)
175
+ check_state :INIT
176
+ @authinfo = [user, passwd, db, flag, charset]
177
+ reset
178
+ init_packet = InitialPacket.parse read
179
+ @server_info = init_packet.server_version
180
+ @server_version = init_packet.server_version.split(/\D/)[0,3].inject{|a,b|a.to_i*100+b.to_i}
181
+ @thread_id = init_packet.thread_id
182
+ client_flags = CLIENT_LONG_PASSWORD | CLIENT_LONG_FLAG | CLIENT_TRANSACTIONS | CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION
183
+ client_flags |= CLIENT_CONNECT_WITH_DB if db
184
+ client_flags |= flag
185
+ @charset = charset
186
+ unless @charset
187
+ @charset = Charset.by_number(init_packet.server_charset)
188
+ @charset.encoding # raise error if unsupported charset
189
+ end
190
+ netpw = encrypt_password passwd, init_packet.scramble_buff
191
+ write AuthenticationPacket.serialize(client_flags, 1024**3, @charset.number, user, netpw, db)
192
+ raise ProtocolError, 'The old style password is not supported' if read.to_s == "\xfe"
193
+ set_state :READY
194
+ end
195
+
196
+ # Quit command
197
+ def quit_command
198
+ synchronize do
199
+ reset
200
+ write [COM_QUIT].pack("C")
201
+ close
202
+ end
203
+ end
204
+
205
+ # Query command
206
+ # === Argument
207
+ # query :: [String] query string
208
+ # === Return
209
+ # [Integer / nil] number of fields of results. nil if no results.
210
+ def query_command(query)
211
+ check_state :READY
212
+ begin
213
+ reset
214
+ write [COM_QUERY, @charset.convert(query)].pack("Ca*")
215
+ get_result
216
+ rescue
217
+ set_state :READY
218
+ raise
219
+ end
220
+ end
221
+
222
+ # get result of query.
223
+ # === Return
224
+ # [integer / nil] number of fields of results. nil if no results.
225
+ def get_result
226
+ begin
227
+ res_packet = ResultPacket.parse read
228
+ if res_packet.field_count.to_i > 0 # result data exists
229
+ set_state :FIELD
230
+ return res_packet.field_count
231
+ end
232
+ if res_packet.field_count.nil? # LOAD DATA LOCAL INFILE
233
+ filename = res_packet.message
234
+ File.open(filename){|f| write f}
235
+ write nil # EOF mark
236
+ read
237
+ end
238
+ @affected_rows, @insert_id, @server_status, @warning_count, @message =
239
+ res_packet.affected_rows, res_packet.insert_id, res_packet.server_status, res_packet.warning_count, res_packet.message
240
+ set_state :READY
241
+ return nil
242
+ rescue
243
+ set_state :READY
244
+ raise
245
+ end
246
+ end
247
+
248
+ # Retrieve n fields
249
+ # === Argument
250
+ # n :: [Integer] number of fields
251
+ # === Return
252
+ # [Array of Mysql::Field] field list
253
+ def retr_fields(n)
254
+ check_state :FIELD
255
+ begin
256
+ fields = n.times.map{Field.new FieldPacket.parse(read)}
257
+ read_eof_packet
258
+ set_state :RESULT
259
+ fields
260
+ rescue
261
+ set_state :READY
262
+ raise
263
+ end
264
+ end
265
+
266
+ # Retrieve all records for simple query
267
+ # === Argument
268
+ # fields :: [Array<Mysql::Field>] number of fields
269
+ # === Return
270
+ # [Array of Array of String] all records
271
+ def retr_all_records(fields)
272
+ check_state :RESULT
273
+ enc = charset.encoding
274
+ begin
275
+ all_recs = []
276
+ until (pkt = read).eof?
277
+ all_recs.push RawRecord.new(pkt, fields, enc)
278
+ end
279
+ pkt.read(3)
280
+ @server_status = pkt.utiny
281
+ all_recs
282
+ ensure
283
+ set_state :READY
284
+ end
285
+ end
286
+
287
+ # Field list command
288
+ # === Argument
289
+ # table :: [String] table name.
290
+ # field :: [String / nil] field name that may contain wild card.
291
+ # === Return
292
+ # [Array of Field] field list
293
+ def field_list_command(table, field)
294
+ synchronize do
295
+ reset
296
+ write [COM_FIELD_LIST, table, 0, field].pack("Ca*Ca*")
297
+ fields = []
298
+ until (data = read).eof?
299
+ fields.push Field.new(FieldPacket.parse(data))
300
+ end
301
+ return fields
302
+ end
303
+ end
304
+
305
+ # Process info command
306
+ # === Return
307
+ # [Array of Field] field list
308
+ def process_info_command
309
+ check_state :READY
310
+ begin
311
+ reset
312
+ write [COM_PROCESS_INFO].pack("C")
313
+ field_count = read.lcb
314
+ fields = field_count.times.map{Field.new FieldPacket.parse(read)}
315
+ read_eof_packet
316
+ set_state :RESULT
317
+ return fields
318
+ rescue
319
+ set_state :READY
320
+ raise
321
+ end
322
+ end
323
+
324
+ # Ping command
325
+ def ping_command
326
+ simple_command [COM_PING].pack("C")
327
+ end
328
+
329
+ # Kill command
330
+ def kill_command(pid)
331
+ simple_command [COM_PROCESS_KILL, pid].pack("CV")
332
+ end
333
+
334
+ # Refresh command
335
+ def refresh_command(op)
336
+ simple_command [COM_REFRESH, op].pack("CC")
337
+ end
338
+
339
+ # Set option command
340
+ def set_option_command(opt)
341
+ simple_command [COM_SET_OPTION, opt].pack("Cv")
342
+ end
343
+
344
+ # Shutdown command
345
+ def shutdown_command(level)
346
+ simple_command [COM_SHUTDOWN, level].pack("CC")
347
+ end
348
+
349
+ # Statistics command
350
+ def statistics_command
351
+ simple_command [COM_STATISTICS].pack("C")
352
+ end
353
+
354
+ # Stmt prepare command
355
+ # === Argument
356
+ # stmt :: [String] prepared statement
357
+ # === Return
358
+ # [Integer] statement id
359
+ # [Integer] number of parameters
360
+ # [Array of Field] field list
361
+ def stmt_prepare_command(stmt)
362
+ synchronize do
363
+ reset
364
+ write [COM_STMT_PREPARE, charset.convert(stmt)].pack("Ca*")
365
+ res_packet = PrepareResultPacket.parse read
366
+ if res_packet.param_count > 0
367
+ res_packet.param_count.times{read} # skip parameter packet
368
+ read_eof_packet
369
+ end
370
+ if res_packet.field_count > 0
371
+ fields = res_packet.field_count.times.map{Field.new FieldPacket.parse(read)}
372
+ read_eof_packet
373
+ else
374
+ fields = []
375
+ end
376
+ return res_packet.statement_id, res_packet.param_count, fields
377
+ end
378
+ end
379
+
380
+ # Stmt execute command
381
+ # === Argument
382
+ # stmt_id :: [Integer] statement id
383
+ # values :: [Array] parameters
384
+ # === Return
385
+ # [Integer] number of fields
386
+ def stmt_execute_command(stmt_id, values)
387
+ check_state :READY
388
+ begin
389
+ reset
390
+ write ExecutePacket.serialize(stmt_id, Mysql::Stmt::CURSOR_TYPE_NO_CURSOR, values)
391
+ get_result
392
+ rescue
393
+ set_state :READY
394
+ raise
395
+ end
396
+ end
397
+
398
+ # Retrieve all records for prepared statement
399
+ # === Argument
400
+ # fields :: [Array of Mysql::Fields] field list
401
+ # charset :: [Mysql::Charset]
402
+ # === Return
403
+ # [Array of Array of Object] all records
404
+ def stmt_retr_all_records(fields, charset)
405
+ check_state :RESULT
406
+ enc = charset.encoding
407
+ begin
408
+ all_recs = []
409
+ until (pkt = read).eof?
410
+ all_recs.push StmtRawRecord.new(pkt, fields, enc)
411
+ end
412
+ all_recs
413
+ ensure
414
+ set_state :READY
415
+ end
416
+ end
417
+
418
+ # Stmt close command
419
+ # === Argument
420
+ # stmt_id :: [Integer] statement id
421
+ def stmt_close_command(stmt_id)
422
+ synchronize do
423
+ reset
424
+ write [COM_STMT_CLOSE, stmt_id].pack("CV")
425
+ end
426
+ end
427
+
428
+ def gc_stmt(stmt_id)
429
+ @gc_stmt_queue.push stmt_id
430
+ end
431
+
432
+ private
433
+
434
+ def check_state(st)
435
+ raise 'command out of sync' unless @state == st
436
+ end
437
+
438
+ def set_state(st)
439
+ @state = st
440
+ if st == :READY
441
+ gc_disabled = GC.disable
442
+ begin
443
+ while st = @gc_stmt_queue.shift
444
+ reset
445
+ write [COM_STMT_CLOSE, st].pack("CV")
446
+ end
447
+ ensure
448
+ GC.enable unless gc_disabled
449
+ end
450
+ end
451
+ end
452
+
453
+ def synchronize
454
+ begin
455
+ check_state :READY
456
+ return yield
457
+ ensure
458
+ set_state :READY
459
+ end
460
+ end
461
+
462
+ # Reset sequence number
463
+ def reset
464
+ @seq = 0 # packet counter. reset by each command
465
+ end
466
+
467
+ # Read one packet data
468
+ # === Return
469
+ # [Packet] packet data
470
+ # === Exception
471
+ # [ProtocolError] invalid packet sequence number
472
+ def read
473
+ data = ''
474
+ len = nil
475
+ begin
476
+ Timeout.timeout @read_timeout do
477
+ header = @sock.read(4)
478
+ raise EOFError unless header && header.length == 4
479
+ len1, len2, seq = header.unpack("CvC")
480
+ len = (len2 << 8) + len1
481
+ raise ProtocolError, "invalid packet: sequence number mismatch(#{seq} != #{@seq}(expected))" if @seq != seq
482
+ @seq = (@seq + 1) % 256
483
+ ret = @sock.read(len)
484
+ raise EOFError unless ret && ret.length == len
485
+ data.concat ret
486
+ end
487
+ rescue EOFError
488
+ raise ClientError::ServerGoneError, 'MySQL server has gone away'
489
+ rescue Timeout::Error
490
+ raise ClientError, "read timeout"
491
+ end while len == MAX_PACKET_LENGTH
492
+
493
+ @sqlstate = "00000"
494
+
495
+ # Error packet
496
+ if data[0] == ?\xff
497
+ f, errno, marker, @sqlstate, message = data.unpack("Cvaa5a*")
498
+ unless marker == "#"
499
+ f, errno, message = data.unpack("Cva*") # Version 4.0 Error
500
+ @sqlstate = ""
501
+ end
502
+ message.force_encoding(@charset.encoding)
503
+ if Mysql::ServerError::ERROR_MAP.key? errno
504
+ raise Mysql::ServerError::ERROR_MAP[errno].new(message, @sqlstate)
505
+ end
506
+ raise Mysql::ServerError.new(message, @sqlstate)
507
+ end
508
+ Packet.new(data)
509
+ end
510
+
511
+ # Write one packet data
512
+ # === Argument
513
+ # data :: [String / IO] packet data. If data is nil, write empty packet.
514
+ def write(data)
515
+ begin
516
+ @sock.sync = false
517
+ if data.nil?
518
+ Timeout.timeout @write_timeout do
519
+ @sock.write [0, 0, @seq].pack("CvC")
520
+ end
521
+ @seq = (@seq + 1) % 256
522
+ else
523
+ data = StringIO.new data if data.is_a? String
524
+ while d = data.read(MAX_PACKET_LENGTH)
525
+ Timeout.timeout @write_timeout do
526
+ @sock.write [d.length%256, d.length/256, @seq].pack("CvC")
527
+ @sock.write d
528
+ end
529
+ @seq = (@seq + 1) % 256
530
+ end
531
+ end
532
+ @sock.sync = true
533
+ Timeout.timeout @write_timeout do
534
+ @sock.flush
535
+ end
536
+ rescue Errno::EPIPE
537
+ raise ClientError::ServerGoneError, 'MySQL server has gone away'
538
+ rescue Timeout::Error
539
+ raise ClientError, "write timeout"
540
+ end
541
+ end
542
+
543
+ # Read EOF packet
544
+ # === Exception
545
+ # [ProtocolError] packet is not EOF
546
+ def read_eof_packet
547
+ raise ProtocolError, "packet is not EOF" unless read.eof?
548
+ end
549
+
550
+ # Send simple command
551
+ # === Argument
552
+ # packet :: [String] packet data
553
+ # === Return
554
+ # [String] received data
555
+ def simple_command(packet)
556
+ synchronize do
557
+ reset
558
+ write packet
559
+ read.to_s
560
+ end
561
+ end
562
+
563
+ # Encrypt password
564
+ # === Argument
565
+ # plain :: [String] plain password.
566
+ # scramble :: [String] scramble code from initial packet.
567
+ # === Return
568
+ # [String] encrypted password
569
+ def encrypt_password(plain, scramble)
570
+ return "" if plain.nil? or plain.empty?
571
+ hash_stage1 = Digest::SHA1.digest plain
572
+ hash_stage2 = Digest::SHA1.digest hash_stage1
573
+ return hash_stage1.unpack("C*").zip(Digest::SHA1.digest(scramble+hash_stage2).unpack("C*")).map{|a,b| a^b}.pack("C*")
574
+ end
575
+
576
+ # Initial packet
577
+ class InitialPacket
578
+ def self.parse(pkt)
579
+ protocol_version = pkt.utiny
580
+ server_version = pkt.string
581
+ thread_id = pkt.ulong
582
+ scramble_buff = pkt.read(8)
583
+ f0 = pkt.utiny
584
+ server_capabilities = pkt.ushort
585
+ server_charset = pkt.utiny
586
+ server_status = pkt.ushort
587
+ f1 = pkt.read(13)
588
+ rest_scramble_buff = pkt.string
589
+ raise ProtocolError, "unsupported version: #{protocol_version}" unless protocol_version == VERSION
590
+ raise ProtocolError, "invalid packet: f0=#{f0}" unless f0 == 0
591
+ scramble_buff.concat rest_scramble_buff
592
+ self.new protocol_version, server_version, thread_id, server_capabilities, server_charset, server_status, scramble_buff
593
+ end
594
+
595
+ attr_reader :protocol_version, :server_version, :thread_id, :server_capabilities, :server_charset, :server_status, :scramble_buff
596
+
597
+ def initialize(*args)
598
+ @protocol_version, @server_version, @thread_id, @server_capabilities, @server_charset, @server_status, @scramble_buff = args
599
+ end
600
+ end
601
+
602
+ # Result packet
603
+ class ResultPacket
604
+ def self.parse(pkt)
605
+ field_count = pkt.lcb
606
+ if field_count == 0
607
+ affected_rows = pkt.lcb
608
+ insert_id = pkt.lcb
609
+ server_status = pkt.ushort
610
+ warning_count = pkt.ushort
611
+ message = pkt.lcs
612
+ return self.new(field_count, affected_rows, insert_id, server_status, warning_count, message)
613
+ elsif field_count.nil? # LOAD DATA LOCAL INFILE
614
+ return self.new(nil, nil, nil, nil, nil, pkt.to_s)
615
+ else
616
+ return self.new(field_count)
617
+ end
618
+ end
619
+
620
+ attr_reader :field_count, :affected_rows, :insert_id, :server_status, :warning_count, :message
621
+
622
+ def initialize(*args)
623
+ @field_count, @affected_rows, @insert_id, @server_status, @warning_count, @message = args
624
+ end
625
+ end
626
+
627
+ # Field packet
628
+ class FieldPacket
629
+ def self.parse(pkt)
630
+ first = pkt.lcs
631
+ db = pkt.lcs
632
+ table = pkt.lcs
633
+ org_table = pkt.lcs
634
+ name = pkt.lcs
635
+ org_name = pkt.lcs
636
+ f0 = pkt.utiny
637
+ charsetnr = pkt.ushort
638
+ length = pkt.ulong
639
+ type = pkt.utiny
640
+ flags = pkt.ushort
641
+ decimals = pkt.utiny
642
+ f1 = pkt.ushort
643
+
644
+ raise ProtocolError, "invalid packet: f1=#{f1}" unless f1 == 0
645
+ default = pkt.lcs
646
+ return self.new(db, table, org_table, name, org_name, charsetnr, length, type, flags, decimals, default)
647
+ end
648
+
649
+ attr_reader :db, :table, :org_table, :name, :org_name, :charsetnr, :length, :type, :flags, :decimals, :default
650
+
651
+ def initialize(*args)
652
+ @db, @table, @org_table, @name, @org_name, @charsetnr, @length, @type, @flags, @decimals, @default = args
653
+ end
654
+ end
655
+
656
+ # Prepare result packet
657
+ class PrepareResultPacket
658
+ def self.parse(pkt)
659
+ raise ProtocolError, "invalid packet" unless pkt.utiny == 0
660
+ statement_id = pkt.ulong
661
+ field_count = pkt.ushort
662
+ param_count = pkt.ushort
663
+ f = pkt.utiny
664
+ warning_count = pkt.ushort
665
+ raise ProtocolError, "invalid packet" unless f == 0x00
666
+ self.new statement_id, field_count, param_count, warning_count
667
+ end
668
+
669
+ attr_reader :statement_id, :field_count, :param_count, :warning_count
670
+
671
+ def initialize(*args)
672
+ @statement_id, @field_count, @param_count, @warning_count = args
673
+ end
674
+ end
675
+
676
+ # Authentication packet
677
+ class AuthenticationPacket
678
+ def self.serialize(client_flags, max_packet_size, charset_number, username, scrambled_password, databasename)
679
+ [
680
+ client_flags,
681
+ max_packet_size,
682
+ Packet.lcb(charset_number),
683
+ "", # always 0x00 * 23
684
+ username,
685
+ Packet.lcs(scrambled_password),
686
+ databasename
687
+ ].pack("VVa*a23Z*A*Z*")
688
+ end
689
+ end
690
+
691
+ # Execute packet
692
+ class ExecutePacket
693
+ def self.serialize(statement_id, cursor_type, values)
694
+ nbm = null_bitmap values
695
+ netvalues = ""
696
+ types = values.map do |v|
697
+ t, n = Protocol.value2net v
698
+ netvalues.concat n if v
699
+ t
700
+ end
701
+ [Mysql::COM_STMT_EXECUTE, statement_id, cursor_type, 1, nbm, 1, types.pack("v*"), netvalues].pack("CVCVa*Ca*a*")
702
+ end
703
+
704
+ # make null bitmap
705
+ #
706
+ # If values is [1, nil, 2, 3, nil] then returns "\x12"(0b10010).
707
+ def self.null_bitmap(values)
708
+ bitmap = values.enum_for(:each_slice,8).map do |vals|
709
+ vals.reverse.inject(0){|b, v|(b << 1 | (v ? 0 : 1))}
710
+ end
711
+ return bitmap.pack("C*")
712
+ end
713
+
714
+ end
715
+ end
716
+
717
+ class RawRecord
718
+ def initialize(packet, fields, encoding)
719
+ @packet, @fields, @encoding = packet, fields, encoding
720
+ end
721
+
722
+ def to_a
723
+ @fields.map do |f|
724
+ if s = @packet.lcs
725
+ unless f.type == Field::TYPE_BIT or f.charsetnr == Charset::BINARY_CHARSET_NUMBER
726
+ s = Charset.convert_encoding(s, @encoding)
727
+ end
728
+ end
729
+ s
730
+ end
731
+ end
732
+ end
733
+
734
+ class StmtRawRecord
735
+ # === Argument
736
+ # pkt :: [Packet]
737
+ # fields :: [Array of Fields]
738
+ # encoding:: [Encoding]
739
+ def initialize(packet, fields, encoding)
740
+ @packet, @fields, @encoding = packet, fields, encoding
741
+ end
742
+
743
+ # Parse statement result packet
744
+ # === Return
745
+ # [Array of Object] one record
746
+ def parse_record_packet
747
+ @packet.utiny # skip first byte
748
+ null_bit_map = @packet.read((@fields.length+7+2)/8).unpack("b*").first
749
+ rec = @fields.each_with_index.map do |f, i|
750
+ if null_bit_map[i+2] == ?1
751
+ nil
752
+ else
753
+ unsigned = f.flags & Field::UNSIGNED_FLAG != 0
754
+ v = Protocol.net2value(@packet, f.type, unsigned)
755
+ if v.is_a? Numeric or v.is_a? Mysql::Time
756
+ v
757
+ elsif f.type == Field::TYPE_BIT or f.charsetnr == Charset::BINARY_CHARSET_NUMBER
758
+ Charset.to_binary(v)
759
+ else
760
+ Charset.convert_encoding(v, @encoding)
761
+ end
762
+ end
763
+ end
764
+ rec
765
+ end
766
+
767
+ alias to_a parse_record_packet
768
+
769
+ end
770
+ end