scraps

Abandon all hope, ye who enter here.
git clone https://git.neptards.moe/neptards/scraps.git
Log | Files | Refs | Submodules | README | LICENSE

parse.rb (5871B)


      1 #! /usr/bin/env ruby
      2 
      3 require "open3"
      4 require "ripl"
      5 require "zlib"
      6 require "base64"
      7 
      8 class Parser
      9   private
     10   FLAGS = {
     11     isbookmark: 1, maydefer: 2, write: 4, writeunknown: 8, isfixedlength: 0x10,
     12     isnullable: 0x20, maybenull: 0x40, islong: 0x80, isrowid: 0x100,
     13     isrowver: 0x200, cachedeferred: 0x1000
     14   }
     15 
     16   def self.bitmask_to_s enum, val
     17     vals = []
     18     enum.each do |n,i|
     19       if val & i != 0
     20         vals << n
     21         val = val & ~i
     22       end
     23     end
     24     vals << val unless val == 0
     25     vals.join ","
     26   end
     27 
     28   def parse_u8;  @out.read(1).ord;             end
     29 
     30   def parse_i16; @out.read(2).unpack("s<")[0]; end
     31   def parse_u16; @out.read(2).unpack("S<")[0]; end
     32 
     33   def parse_i32; @out.read(4).unpack("l<")[0]; end
     34   def parse_u32; @out.read(4).unpack("L<")[0]; end
     35   def parse_f32; @out.read(4).unpack("e")[0];  end
     36 
     37   def parse_f64; @out.read(8).unpack("E")[0];  end
     38 
     39   def parse_crazy_bool; parse_u16 != 0; end
     40 
     41   def parse_genstring encoding
     42     @out.read(parse_u32).force_encoding encoding
     43   end
     44 
     45   def parse_wstring; parse_genstring Encoding::UTF_16LE; end
     46   def parse_string;  parse_genstring Encoding::UTF_8;    end
     47   def parse_bin;     parse_genstring Encoding::BINARY;   end
     48 
     49   def parse_error code
     50     case code
     51     when 0xff
     52       hr = parse_u32
     53       msg = parse_wstring
     54       printf "HRESULT 0x%x %s\n", hr, msg.encode(Encoding::UTF_8)
     55       throw :failed
     56     when 0xfe
     57       msg = parse_string
     58       printf "ERROR %s\n", msg
     59       throw :failed
     60     else
     61       fail "Unknown code #{code}"
     62     end
     63   end
     64 
     65   def parse_blob
     66     buf = "".force_encoding Encoding::BINARY
     67     loop do
     68       code = parse_u8
     69       case code
     70       when 0
     71         return buf
     72       when 1
     73         buf << parse_bin
     74       else
     75         parse_error code
     76       end
     77     end
     78   end
     79 
     80   def self.uniform_method name
     81     fun = instance_method name
     82     lambda do |inst, *args, &blk|
     83       fun.bind_call inst, *args, &blk
     84     end
     85   end
     86 
     87   def parse_nil; end
     88 
     89   def self.with_len_check exp_len, fun
     90     fun = instance_method fun
     91     lambda do |inst|
     92       len = inst.send :parse_u32
     93       fail unless len == exp_len
     94       fun.bind_call inst
     95     end
     96   end
     97 
     98   TYPES = {
     99     2 => with_len_check(2, :parse_i16),
    100     3 => with_len_check(4, :parse_i32),
    101     4 => with_len_check(4, :parse_f32),
    102     5 => with_len_check(8, :parse_f64),
    103     11 => with_len_check(2, :parse_crazy_bool),
    104     # 72 => method(:parse_bin), # GUID
    105     128 => uniform_method(:parse_bin),
    106     130 => uniform_method(:parse_wstring),
    107   }
    108 
    109   # not Struct because braindead ap ignores custom inspect on Structs...
    110   class Result
    111     attr_accessor :header, :rows
    112     def initialize header, rows
    113       @header = header
    114       @rows = rows
    115     end
    116     def inspect
    117       "Result(cols=#{header.size}, rows=#{rows.size})"
    118     end
    119     alias :to_s :inspect
    120   end
    121   ColumnInfo = Struct.new :type, :flags, :name
    122   Cell = Struct.new :status, :value
    123 
    124   def parse_rows types
    125     0.step do |r|
    126       code = parse_u8
    127       case code
    128       when 0
    129         puts "--END"
    130         return
    131 
    132       when 1
    133         l = [] if @config.save_last
    134         types.each_with_index do |t, i|
    135           c2 = parse_u8
    136           case c2
    137           when 0
    138             stat = parse_u32
    139             parser = TYPES[t]
    140             parser = Parser.with_len_check(0, :parse_nil) if stat == 3
    141             unless parser
    142               parser = Parser.uniform_method :parse_bin
    143               puts "Unhandled type #{t}!"
    144             end
    145             val = parser[self]
    146             l << Cell.new(stat, val) if l
    147             puts "#{r}/#{i}: #{stat} #{val.inspect}"
    148           when 1
    149             stat = parse_u32
    150             val = parse_blob
    151             val.force_encoding Encoding::UTF_16LE if t == 130 # hack
    152             l << Cell.new(stat, val) if l
    153 
    154             max = @config.max_blob_display
    155             if max && val.size > max
    156               val = val[0,max] + "...".encode(val.encoding)
    157             end
    158             puts "#{r}/#{i}: #{stat} BLOB(#{val.inspect})"
    159           else
    160             parse_error c2
    161           end
    162         end
    163         @last.rows << l if l
    164 
    165       else
    166         parse_error code
    167       end
    168     end
    169   end
    170 
    171   def send_cmd cmd
    172     cmd = cmd.encode Encoding::UTF_16LE
    173     @in.write [0, cmd.bytesize, cmd].pack "cla*"
    174   end
    175 
    176   def parse_cmd_res
    177     code = parse_u8
    178     case code
    179     when 0
    180       @last = nil if @config.save_last
    181       puts "OK"
    182 
    183     when 1
    184       puts "RES"
    185       n_cols = parse_u32
    186       @last = Result.new([], []) if @config.save_last
    187       types = n_cols.times.map do |i|
    188         type = parse_u32
    189         flags = parse_u32
    190         name = parse_wstring
    191         @last.header << ColumnInfo.new(type, flags, name) if @config.save_last
    192         puts "#{i}: #{type}/#{Parser.bitmask_to_s FLAGS, flags} #{name.inspect}"
    193         type
    194       end
    195 
    196       parse_rows types
    197     else
    198       parse_error code
    199     end
    200   end
    201 
    202   class Config
    203     def initialize
    204       @max_blob_display = 32
    205       @save_last = true
    206     end
    207 
    208     attr_accessor :max_blob_display
    209     attr_accessor :save_last
    210   end
    211 
    212   public
    213   def initialize stdin, stdout
    214     @config = Config.new
    215     @in = stdin
    216     @out = stdout
    217   end
    218   attr_reader :config, :last
    219 
    220   def sql cmd
    221     send_cmd cmd
    222     catch(:failed) { parse_cmd_res }
    223     @last if @config.save_last
    224   end
    225 end
    226 
    227 class Cmds
    228   def initialize *args; @parser = Parser.new *args; end
    229   (Parser.public_instance_methods - Object.methods).each do |m|
    230     define_method m do |*args, &blk|
    231       @parser.send m, *args, &blk
    232     end
    233   end
    234 
    235   def gunzip_base64 data
    236     Zlib.gunzip Base64.decode64(data)[4..-1]
    237   end
    238 
    239   def parse_xml data
    240     require "nokogiri"
    241     Nokogiri.XML Zlib.gunzip(Base64.decode64(data)[4..-1]), &:nocdata
    242   end
    243 end
    244 
    245 fail "Usage: #$0 file" unless ARGV.size == 1
    246 Open3.popen3 "./load.exe", ARGV[0] do |stdin, stdout, thr|
    247   c = Cmds.new stdin, stdout
    248   Ripl.start binding: c.instance_eval { binding }
    249   stdin.close
    250 end