cf58a5
#!/usr/bin/env python
cf58a5
# -*- Mode: Python; tab-width: 4 -*-
cf58a5
#
cf58a5
# Cyrus Imapd Skiplist db recovery tool
cf58a5
#
cf58a5
# Copyright (C) 2004 Gianluigi Tiesi <sherpya@netfarm.it>
cf58a5
# Copyright (C) 2004 NetFarm S.r.l.  [http://www.netfarm.it]
cf58a5
#
cf58a5
# This program is free software; you can redistribute it and/or modify
cf58a5
# it under the terms of the GNU General Public License as published by the
cf58a5
# Free Software Foundation; either version 2, or (at your option) any later
cf58a5
# version.
cf58a5
#
cf58a5
# This program is distributed in the hope that it will be useful, but
cf58a5
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY
cf58a5
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
cf58a5
# for more details.
cf58a5
# ======================================================================
cf58a5
cf58a5
__version__= '0.1'
cf58a5
__doc__="""Cyrus skiplist db recover"""
cf58a5
cf58a5
from sys import argv,exit,stdout,stderr
cf58a5
from struct import unpack
cf58a5
from time import localtime, strftime
cf58a5
cf58a5
### User Conf
cf58a5
debug = 0
cf58a5
###
cf58a5
cf58a5
TIMEFMT ='%a, %d %b %Y %H:%M:%S %z'
cf58a5
PADDING = '\xff' * 4
cf58a5
INORDER = 1
cf58a5
ADD     = 2
cf58a5
DELETE  = 4
cf58a5
COMMIT  = 255
cf58a5
DUMMY   = 257
cf58a5
HEADER  = -1
cf58a5
MAIN    = -2
cf58a5
cf58a5
types = {
cf58a5
    1:   'INORDER',
cf58a5
    2:   'ADD',
cf58a5
    4:   'DELETE',
cf58a5
    255: 'COMMIT',
cf58a5
    257: 'DUMMY',
cf58a5
    -1:  'HEADER',
cf58a5
    -2:  '*'
cf58a5
    }
cf58a5
cf58a5
def log(rtype, text):
cf58a5
    global debug
cf58a5
    if debug:
cf58a5
        out = '[%s] %s\n' % (types[rtype], text)
cf58a5
        stdout.write(out)
cf58a5
        stdout.flush()
cf58a5
cf58a5
def roundto4(value):
cf58a5
    if value % 4:
cf58a5
        return ((value / 4) + 1) * 4
cf58a5
    return value
cf58a5
cf58a5
def get_header(fp):
cf58a5
    #### Magic ??
cf58a5
    fp.seek(4)
cf58a5
    
cf58a5
    sign = fp.read(16)
cf58a5
    log(HEADER, sign[:-3])
cf58a5
cf58a5
    version = unpack('>I', fp.read(4))[0]
cf58a5
    version_minor = unpack('>I', fp.read(4))[0]
cf58a5
cf58a5
    log(HEADER, 'Version %d,%d' % (version, version_minor))
cf58a5
cf58a5
    maxlevel = unpack('>I', fp.read(4))[0]
cf58a5
    curlevel = unpack('>I', fp.read(4))[0]
cf58a5
cf58a5
    log(HEADER, 'Level %d/%d' % (curlevel, maxlevel))
cf58a5
cf58a5
    listsize = unpack('>I', fp.read(4))[0]
cf58a5
    log(HEADER, 'List size %d' % listsize)
cf58a5
cf58a5
    logstart = unpack('>I', fp.read(4))[0]
cf58a5
    log(HEADER, 'Offset %d' % logstart)
cf58a5
    
cf58a5
    lastrecovery = localtime(unpack('>I', fp.read(4))[0])
cf58a5
    lastrecovery = strftime(TIMEFMT, lastrecovery)
cf58a5
cf58a5
    log(HEADER, 'Last Recovery %s' % lastrecovery)
cf58a5
cf58a5
    return { 'version'    : [version, version_minor],
cf58a5
             'level'      : [curlevel, maxlevel],
cf58a5
             'listsize'   : listsize,
cf58a5
             'logstart'   : logstart,
cf58a5
             'lastrecover': lastrecovery
cf58a5
             }
cf58a5
cf58a5
def getkeys(fp):
cf58a5
    values = []
cf58a5
    keys = {}
cf58a5
    keystring = ''
cf58a5
    datastring = ''
cf58a5
cf58a5
    while 1:
cf58a5
        log(MAIN, '-'*78)
cf58a5
cf58a5
        stype = fp.read(4)
cf58a5
cf58a5
        ### EOF
cf58a5
        if len(stype) != 4:
cf58a5
            break
cf58a5
cf58a5
        rtype = unpack('>I', stype)[0]
cf58a5
        if not types.has_key(rtype):
cf58a5
            log(MAIN, 'Invalid type %d' % rtype)
cf58a5
            continue
cf58a5
        
cf58a5
        log(rtype, 'Record type %s' % types[rtype])
cf58a5
cf58a5
        if rtype == DELETE:
cf58a5
            ptr = unpack('>I', fp.read(4))[0]
cf58a5
            log(rtype, 'DELETE %d (0x%x)' % (ptr, ptr))
cf58a5
            continue
cf58a5
cf58a5
        if rtype == COMMIT:
cf58a5
            continue
cf58a5
        
cf58a5
        ksize = unpack('>I', fp.read(4))[0]
cf58a5
        log(rtype, 'Key size %d (%d)' % (ksize, roundto4(ksize)))
cf58a5
cf58a5
        if ksize:
cf58a5
            keystring = fp.read(roundto4(ksize))[:ksize]
cf58a5
            log(rtype, 'Key String %s' % keystring)
cf58a5
cf58a5
        datasize = unpack('>I', fp.read(4))[0]
cf58a5
        log(rtype, 'Data size %d (%d)' % (datasize, roundto4(datasize)))
cf58a5
                
cf58a5
        if datasize:
cf58a5
            datastring = fp.read(roundto4(datasize))[:datasize]
cf58a5
            log(rtype, 'Data String %s' % datastring)
cf58a5
cf58a5
        n = 0
cf58a5
        while 1:
cf58a5
            str_p = fp.read(4)
cf58a5
            if str_p == PADDING:
cf58a5
                break
cf58a5
            spointer = unpack('>I', str_p)[0]
cf58a5
            n = n +1
cf58a5
            if spointer: log(rtype, 'Skip pointer %d' % spointer)
cf58a5
            
cf58a5
        log(rtype, 'Total Skip pointers: %d' % n)
cf58a5
cf58a5
        if rtype != DUMMY:
cf58a5
            if keystring not in values:
cf58a5
                values.append(keystring)
cf58a5
            keys[keystring] = datastring
cf58a5
cf58a5
    return values, keys
cf58a5
cf58a5
if __name__ == '__main__':
cf58a5
    if len(argv) != 2:
cf58a5
        print 'Usage: %s skiplist.file' % argv[0]
cf58a5
        exit()
cf58a5
cf58a5
    fp = open(argv[1], 'rb')
cf58a5
    header = get_header(fp)
cf58a5
    values, keys = getkeys(fp)
cf58a5
    fp.close()
cf58a5
cf58a5
    if debug: exit()
cf58a5
    for v in values:
cf58a5
        print '%s\t%s' % (v, keys[v])