121 lines
3.5 KiB
Python
121 lines
3.5 KiB
Python
# The contents of this file are subject to the BitTorrent Open Source License
|
|
# Version 1.1 (the License). You may not copy or use this file, in either
|
|
# source code or executable form, except in compliance with the License. You
|
|
# may obtain a copy of the License at http://www.bittorrent.com/license/.
|
|
#
|
|
# Software distributed under the License is distributed on an AS IS basis,
|
|
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
# for the specific language governing rights and limitations under the
|
|
# License.
|
|
|
|
from sha import sha
|
|
from random import randint
|
|
|
|
#this is ugly, hopefully os.entropy will be in 2.4
|
|
try:
|
|
from entropy import entropy
|
|
except ImportError:
|
|
def entropy(n):
|
|
s = ''
|
|
for i in range(n):
|
|
s += chr(randint(0,255))
|
|
return s
|
|
|
|
def intify(hstr):
|
|
"""20 bit hash, big-endian -> long python integer"""
|
|
assert len(hstr) == 20
|
|
return long(hstr.encode('hex'), 16)
|
|
|
|
def stringify(num):
|
|
"""long int -> 20-character string"""
|
|
str = hex(num)[2:]
|
|
if str[-1] == 'L':
|
|
str = str[:-1]
|
|
if len(str) % 2 != 0:
|
|
str = '0' + str
|
|
str = str.decode('hex')
|
|
return (20 - len(str)) *'\x00' + str
|
|
|
|
def distance(a, b):
|
|
"""distance between two 160-bit hashes expressed as 20-character strings"""
|
|
return intify(a) ^ intify(b)
|
|
|
|
|
|
def newID():
|
|
"""returns a new pseudorandom globally unique ID string"""
|
|
h = sha()
|
|
h.update(entropy(20))
|
|
return h.digest()
|
|
|
|
def newIDInRange(min, max):
|
|
return stringify(randRange(min,max))
|
|
|
|
def randRange(min, max):
|
|
return min + intify(newID()) % (max - min)
|
|
|
|
def newTID():
|
|
return randRange(-2**30, 2**30)
|
|
|
|
### Test Cases ###
|
|
import unittest
|
|
|
|
class NewID(unittest.TestCase):
|
|
def testLength(self):
|
|
self.assertEqual(len(newID()), 20)
|
|
def testHundreds(self):
|
|
for x in xrange(100):
|
|
self.testLength
|
|
|
|
class Intify(unittest.TestCase):
|
|
known = [('\0' * 20, 0),
|
|
('\xff' * 20, 2L**160 - 1),
|
|
]
|
|
def testKnown(self):
|
|
for str, value in self.known:
|
|
self.assertEqual(intify(str), value)
|
|
def testEndianessOnce(self):
|
|
h = newID()
|
|
while h[-1] == '\xff':
|
|
h = newID()
|
|
k = h[:-1] + chr(ord(h[-1]) + 1)
|
|
self.assertEqual(intify(k) - intify(h), 1)
|
|
def testEndianessLots(self):
|
|
for x in xrange(100):
|
|
self.testEndianessOnce()
|
|
|
|
class Disantance(unittest.TestCase):
|
|
known = [
|
|
(("\0" * 20, "\xff" * 20), 2**160L -1),
|
|
((sha("foo").digest(), sha("foo").digest()), 0),
|
|
((sha("bar").digest(), sha("bar").digest()), 0)
|
|
]
|
|
def testKnown(self):
|
|
for pair, dist in self.known:
|
|
self.assertEqual(distance(pair[0], pair[1]), dist)
|
|
def testCommutitive(self):
|
|
for i in xrange(100):
|
|
x, y, z = newID(), newID(), newID()
|
|
self.assertEqual(distance(x,y) ^ distance(y, z), distance(x, z))
|
|
|
|
class RandRange(unittest.TestCase):
|
|
def testOnce(self):
|
|
a = intify(newID())
|
|
b = intify(newID())
|
|
if a < b:
|
|
c = randRange(a, b)
|
|
self.assertEqual(a <= c < b, 1, "output out of range %d %d %d" % (b, c, a))
|
|
else:
|
|
c = randRange(b, a)
|
|
assert b <= c < a, "output out of range %d %d %d" % (b, c, a)
|
|
|
|
def testOneHundredTimes(self):
|
|
for i in xrange(100):
|
|
self.testOnce()
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|
|
|
|
|