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
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
|