1
0

khash.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. # The contents of this file are subject to the BitTorrent Open Source License
  2. # Version 1.1 (the License). You may not copy or use this file, in either
  3. # source code or executable form, except in compliance with the License. You
  4. # may obtain a copy of the License at http://www.bittorrent.com/license/.
  5. #
  6. # Software distributed under the License is distributed on an AS IS basis,
  7. # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  8. # for the specific language governing rights and limitations under the
  9. # License.
  10. from sha import sha
  11. from random import randint
  12. #this is ugly, hopefully os.entropy will be in 2.4
  13. try:
  14. from entropy import entropy
  15. except ImportError:
  16. def entropy(n):
  17. s = ''
  18. for i in range(n):
  19. s += chr(randint(0,255))
  20. return s
  21. def intify(hstr):
  22. """20 bit hash, big-endian -> long python integer"""
  23. assert len(hstr) == 20
  24. return long(hstr.encode('hex'), 16)
  25. def stringify(num):
  26. """long int -> 20-character string"""
  27. str = hex(num)[2:]
  28. if str[-1] == 'L':
  29. str = str[:-1]
  30. if len(str) % 2 != 0:
  31. str = '0' + str
  32. str = str.decode('hex')
  33. return (20 - len(str)) *'\x00' + str
  34. def distance(a, b):
  35. """distance between two 160-bit hashes expressed as 20-character strings"""
  36. return intify(a) ^ intify(b)
  37. def newID():
  38. """returns a new pseudorandom globally unique ID string"""
  39. h = sha()
  40. h.update(entropy(20))
  41. return h.digest()
  42. def newIDInRange(min, max):
  43. return stringify(randRange(min,max))
  44. def randRange(min, max):
  45. return min + intify(newID()) % (max - min)
  46. def newTID():
  47. return randRange(-2**30, 2**30)
  48. ### Test Cases ###
  49. import unittest
  50. class NewID(unittest.TestCase):
  51. def testLength(self):
  52. self.assertEqual(len(newID()), 20)
  53. def testHundreds(self):
  54. for x in xrange(100):
  55. self.testLength
  56. class Intify(unittest.TestCase):
  57. known = [('\0' * 20, 0),
  58. ('\xff' * 20, 2L**160 - 1),
  59. ]
  60. def testKnown(self):
  61. for str, value in self.known:
  62. self.assertEqual(intify(str), value)
  63. def testEndianessOnce(self):
  64. h = newID()
  65. while h[-1] == '\xff':
  66. h = newID()
  67. k = h[:-1] + chr(ord(h[-1]) + 1)
  68. self.assertEqual(intify(k) - intify(h), 1)
  69. def testEndianessLots(self):
  70. for x in xrange(100):
  71. self.testEndianessOnce()
  72. class Disantance(unittest.TestCase):
  73. known = [
  74. (("\0" * 20, "\xff" * 20), 2**160L -1),
  75. ((sha("foo").digest(), sha("foo").digest()), 0),
  76. ((sha("bar").digest(), sha("bar").digest()), 0)
  77. ]
  78. def testKnown(self):
  79. for pair, dist in self.known:
  80. self.assertEqual(distance(pair[0], pair[1]), dist)
  81. def testCommutitive(self):
  82. for i in xrange(100):
  83. x, y, z = newID(), newID(), newID()
  84. self.assertEqual(distance(x,y) ^ distance(y, z), distance(x, z))
  85. class RandRange(unittest.TestCase):
  86. def testOnce(self):
  87. a = intify(newID())
  88. b = intify(newID())
  89. if a < b:
  90. c = randRange(a, b)
  91. self.assertEqual(a <= c < b, 1, "output out of range %d %d %d" % (b, c, a))
  92. else:
  93. c = randRange(b, a)
  94. assert b <= c < a, "output out of range %d %d %d" % (b, c, a)
  95. def testOneHundredTimes(self):
  96. for i in xrange(100):
  97. self.testOnce()
  98. if __name__ == '__main__':
  99. unittest.main()