You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

251 lines
5.7 KiB
Ruby

#! /usr/bin/env ruby
require "open3"
require "ripl"
require "zlib"
require "base64"
class Parser
private
FLAGS = {
isbookmark: 1, maydefer: 2, write: 4, writeunknown: 8, isfixedlength: 0x10,
isnullable: 0x20, maybenull: 0x40, islong: 0x80, isrowid: 0x100,
isrowver: 0x200, cachedeferred: 0x1000
}
def self.bitmask_to_s enum, val
vals = []
enum.each do |n,i|
if val & i != 0
vals << n
val = val & ~i
end
end
vals << val unless val == 0
vals.join ","
end
def parse_u8; @out.read(1).ord; end
def parse_i16; @out.read(2).unpack("s<")[0]; end
def parse_u16; @out.read(2).unpack("S<")[0]; end
def parse_i32; @out.read(4).unpack("l<")[0]; end
def parse_u32; @out.read(4).unpack("L<")[0]; end
def parse_f32; @out.read(4).unpack("e")[0]; end
def parse_f64; @out.read(8).unpack("E")[0]; end
def parse_crazy_bool; parse_u16 != 0; end
def parse_genstring encoding
@out.read(parse_u32).force_encoding encoding
end
def parse_wstring; parse_genstring Encoding::UTF_16LE; end
def parse_string; parse_genstring Encoding::UTF_8; end
def parse_bin; parse_genstring Encoding::BINARY; end
def parse_error code
case code
when 0xff
hr = parse_u32
msg = parse_wstring
printf "HRESULT 0x%x %s\n", hr, msg.encode(Encoding::UTF_8)
throw :failed
when 0xfe
msg = parse_string
printf "ERROR %s\n", msg
throw :failed
else
fail "Unknown code #{code}"
end
end
def parse_blob
buf = "".force_encoding Encoding::BINARY
loop do
code = parse_u8
case code
when 0
return buf
when 1
buf << parse_bin
else
parse_error code
end
end
end
def self.uniform_method name
fun = instance_method name
lambda do |inst, *args, &blk|
fun.bind_call inst, *args, &blk
end
end
def parse_nil; end
def self.with_len_check exp_len, fun
fun = instance_method fun
lambda do |inst|
len = inst.send :parse_u32
fail unless len == exp_len
fun.bind_call inst
end
end
TYPES = {
2 => with_len_check(2, :parse_i16),
3 => with_len_check(4, :parse_i32),
4 => with_len_check(4, :parse_f32),
5 => with_len_check(8, :parse_f64),
11 => with_len_check(2, :parse_crazy_bool),
# 72 => method(:parse_bin), # GUID
128 => uniform_method(:parse_bin),
130 => uniform_method(:parse_wstring),
}
# not Struct because braindead ap ignores custom inspect on Structs...
class Result
attr_accessor :header, :rows
def initialize header, rows
@header = header
@rows = rows
end
def inspect
"Result(cols=#{header.size}, rows=#{rows.size})"
end
alias :to_s :inspect
end
ColumnInfo = Struct.new :type, :flags, :name
Cell = Struct.new :status, :value
def parse_rows types
0.step do |r|
code = parse_u8
case code
when 0
puts "--END"
return
when 1
l = [] if @config.save_last
types.each_with_index do |t, i|
c2 = parse_u8
case c2
when 0
stat = parse_u32
parser = TYPES[t]
parser = Parser.with_len_check(0, :parse_nil) if stat == 3
unless parser
parser = Parser.uniform_method :parse_bin
puts "Unhandled type #{t}!"
end
val = parser[self]
l << Cell.new(stat, val) if l
puts "#{r}/#{i}: #{stat} #{val.inspect}"
when 1
stat = parse_u32
val = parse_blob
val.force_encoding Encoding::UTF_16LE if t == 130 # hack
l << Cell.new(stat, val) if l
max = @config.max_blob_display
if max && val.size > max
val = val[0,max] + "...".encode(val.encoding)
end
puts "#{r}/#{i}: #{stat} BLOB(#{val.inspect})"
else
parse_error c2
end
end
@last.rows << l if l
else
parse_error code
end
end
end
def send_cmd cmd
cmd = cmd.encode Encoding::UTF_16LE
@in.write [0, cmd.bytesize, cmd].pack "cla*"
end
def parse_cmd_res
code = parse_u8
case code
when 0
@last = nil if @config.save_last
puts "OK"
when 1
puts "RES"
n_cols = parse_u32
@last = Result.new([], []) if @config.save_last
types = n_cols.times.map do |i|
type = parse_u32
flags = parse_u32
name = parse_wstring
@last.header << ColumnInfo.new(type, flags, name) if @config.save_last
puts "#{i}: #{type}/#{Parser.bitmask_to_s FLAGS, flags} #{name.inspect}"
type
end
parse_rows types
else
parse_error code
end
end
class Config
def initialize
@max_blob_display = 32
@save_last = true
end
attr_accessor :max_blob_display
attr_accessor :save_last
end
public
def initialize stdin, stdout
@config = Config.new
@in = stdin
@out = stdout
end
attr_reader :config, :last
def sql cmd
send_cmd cmd
catch(:failed) { parse_cmd_res }
@last if @config.save_last
end
end
class Cmds
def initialize *args; @parser = Parser.new *args; end
(Parser.public_instance_methods - Object.methods).each do |m|
define_method m do |*args, &blk|
@parser.send m, *args, &blk
end
end
def gunzip_base64 data
Zlib.gunzip Base64.decode64(data)[4..-1]
end
def parse_xml data
require "nokogiri"
Nokogiri.XML Zlib.gunzip(Base64.decode64(data)[4..-1]), &:nocdata
end
end
fail "Usage: #$0 file" unless ARGV.size == 1
Open3.popen3 "./load.exe", ARGV[0] do |stdin, stdout, thr|
c = Cmds.new stdin, stdout
Ripl.start binding: c.instance_eval { binding }
stdin.close
end