cdb format: hash table slots
David Farrar <[email protected]> Fri, 11 Apr 2008 18:18:42 +0100
| Newsgroups | gmane.comp.djb.cdb |
|---|---|
| Message-ID | <[email protected]> |
This is a multi-part message in MIME format.
--------------090208030301020206070508
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
Content-Transfer-Encoding: 7bit
Hello,
I've been looking recently at the cdb format and come across a
discrepancy between the actual output of cdbmake and the output I
expected. Hopefully somebody will be able to tell me where I have gone
wrong.
If I create a small cdb file with cdbmake, using the records
+13,3:thomas.mangin->foo
+11,3:markcowgill->bar
so that the hashes of both keys are stored in the same hash table, I
notice that this table is 4 slots long.
Parsing the cdb file, I can see that the four slots are
2417049413, pointer to record with key markcowgill,
empty slot
empty slot
8948549, pointer to record with key thomas.mangin
where the value preceeding each pointer is a stored hash of the
respective key.
Taken from http://cr.yp.to/cdb/cdb.txt,
"""
A record is located as follows. Compute the hash value of the key in
the record. The hash value modulo 256 is the number of a hash table.
The hash value divided by 256, modulo the length of that table, is a
slot number. Probe that slot, the next higher slot, and so on, until
you find the record or run into an empty slot.
"""
Reading this paragraph, I expected to be able able to find the entry for
'markcowgill' by seeking to the starting position of the hash table +
(2*4) * n bytes, where n = (hash / 256) % 4.
In this particular case, (hash / 256) % 4 = (2417049413 / 256) % 4 = 3,
meaning that I skip past the value I'm looking for (jumping straight to
'thomas.mangin') and falsely report that the key does not exist.
I can still retrieve a key by hardcoding the slot position to 0 or even
reading each record sequentially but this doesn't seem to be the best
way to read quickly and I would appreciate it if somebody could put me
ok the right track.
In case it helps, I have written a rough and ready python cdb
implementation to check the data and rewrite it in a format that does
what I expect. I'm not sure that the write function is correct but I ran
it on a fairly large dataset and it gave me the results I wanted.
Setting self.broken on line 31 to False forces slot number = 0 when
performing a hash lookup and is the only way I can parse data generated
by cdbmake
David
--------------090208030301020206070508
Content-Type: text/x-python;
name="cdb.py"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="cdb.py"
#
# Copyright 2008 Exa Networks
# This code is placed in the public domain
#
import struct
class HashSlot:
def __init__(self, value, position, total):
self.position = position
self.value = value
self.minposition = (self.value /256) % total
def __cmp__(self, other):
if self.minposition < other.minposition:
return -1
if self.minposition > other.minposition:
return 1
return 0
def __str__(self):
return ''.join(struct.unpack('ssssssss', struct.pack('<II', self.value, self.position)))
def __repr__(self):
return ' '.join((str(v) for v in (self.value, self.position, self.minposition)))
class cdb (dict):
def __init__(self, filename):
self.filename = filename
self.fd = open(filename, 'r')
self.debug = True
self.broken = True
def __cdbhash(self, key):
h = 5381 # mmmmagic
for c in key:
h = ((h << 5) + h) ^ ord(c)
h = h & 0xffffffff
return h
def __find_hash_offset(self):
self.fd.seek(0)
d = self.fd.read(4)
if not d.__len__() == 4:
raise DataError, 'The specified file is not a valid cdb file'
d = struct.unpack('<I', struct.pack('ssss', *(b for b in d)))
return d[0]
def __read_record(self, pos):
self.fd.seek(pos)
kl = struct.unpack('<I', struct.pack('ssss', *(b for b in self.fd.read(4))))[0]
dl = struct.unpack('<I', struct.pack('ssss', *(b for b in self.fd.read(4))))[0]
key = self.fd.read(kl)
data = self.fd.read(dl)
return 8 + kl + dl, key, data
def __getitem__(self, item):
hash = self.__cdbhash(item)
n = hash % 256
self.fd.seek(8*n)
hash_pos = struct.unpack('<I', struct.pack('ssss', *(b for b in self.fd.read(4))))[0]
hash_len = struct.unpack('<I', struct.pack('ssss', *(b for b in self.fd.read(4))))[0]
if hash_len == 0:
raise KeyError, item
self.debug = n == 69
slot = (hash/256) % hash_len
if self.broken:
slot = 0
if self.debug:
print item, '\ttable number is', n, '\thash', hash, 'is at byte postion', hash_pos, '\tslot offset is +%s'% slot, 'with', hash_len, 'slots'
hash_pos += 8*slot
for i in xrange(hash_len - slot):
self.fd.seek(hash_pos + 8*i)
stored = struct.unpack('<I', struct.pack('ssss', *(b for b in self.fd.read(4))))[0]
pos = struct.unpack('<I', struct.pack('ssss', *(b for b in self.fd.read(4))))[0]
if pos == 0: # from http://cr.yp.to/cdb/cdb.txt
if self.debug:
print "FOUND\tPADDING"
continue # "If the byte position is 0, the slot is empty."
if True or stored == hash:
len, key, value = self.__read_record(pos)
if self.debug:
print "FOUND\t", key
if key == item:
if self.debug:
print
print
return value
raise KeyError, item
def iteritems(self):
end = self.__find_hash_offset() - 2048
read = 0
while read < end:
len, key, value = self.__read_record(2048+read)
read += len
yield key, value
def items(self):
return [item for item in self.iteritems()]
def iterkeys(self):
return (key for key, value in self.iteritems())
def keys(self):
return [key for key, value in self.iteritems()]
def itervalues(self):
return (value for key, value in self.iteritems())
def values(self):
return [value for key, value in self.iteritems()]
def write(self):
wfd = open(self.filename+'.tmp', 'w')
tables = {}
for i in xrange(256):
tables[i] = []
wfd.seek(2048)
written = 0
for key, value in self.iteritems():
pos = wfd.tell()
kl = key.__len__()
vl = value.__len__()
key_len = ''.join(struct.unpack('ssss', struct.pack('<I', kl)))
value_len = ''.join(struct.unpack('ssss', struct.pack('<I', vl)))
wfd.write(key_len + value_len)
wfd.write(key)
wfd.write(value)
written += kl + vl + 8
hash = self.__cdbhash(key)
n = hash % 256
tables[n].append((hash, pos))
for i in xrange(256):
table = tables[i]
count = len(table)
hashtable = [HashSlot(hash, pos, count) for hash, pos in table]
hashtable.sort()
added = 0
while True:
hashtable = [HashSlot(hash, pos, count) for hash, pos in table]
hashtable.sort()
ok = True
for n in xrange(hashtable.__len__()):
if hashtable[n].minposition > n+added:
ok = False
break
if ok:
break
count += 1
added += 1
tables[i] = [wfd.tell(), count]
written = 0
for slot in hashtable:
minpos = slot.minposition
if minpos > written:
for j in xrange(minpos - written):
wfd.write('\0\0\0\0\0\0\0\0')
written +=1
wfd.write(str(slot))
written += 1
for j in xrange(count - written):
wfd.write('\0\0\0\0\0\0\0\0')
wfd.seek(0)
for i in xrange(256):
pos, count = tables[i]
s = ''.join(struct.unpack('ssssssss', struct.pack('<II', pos, count)))
wfd.write(s)
wfd.close()
if __name__ == '__main__':
filename = '/home/david/exapasswd.cdb.test'
c = cdb(filename)
for key in c.iterkeys():
c[key]
c.write()
d = cdb(filename+'.tmp')
for key in d.iterkeys():
d[key]
x = dict(c.items())
y = dict(d.items())
print x == y
--------------090208030301020206070508--