The little things give you away... A collection of various small helper stuff
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

151 lines
3.3 KiB

  1. #!/usr/bin/env python3
  2. import enum
  3. import hashlib
  4. import sys
  5. class ParserState(enum.Enum):
  6. NONE = 0
  7. DICTIONARY = 1
  8. LIST = 2
  9. class Placeholder:
  10. def __init__(self, s):
  11. self._s = s
  12. def __repr__(self):
  13. return self._s
  14. dictEntry = Placeholder('dict')
  15. listEntry = Placeholder('list')
  16. class CopyingFileReader:
  17. '''All reads to the underlying file-like object are copied to write, which must be a callable accepting the data.'''
  18. def __init__(self, fp, write):
  19. self.fp = fp
  20. self.write = write
  21. def read(self, *args, **kwargs):
  22. data = self.fp.read(*args, **kwargs)
  23. self.write(data)
  24. return data
  25. def read_str(fp, c):
  26. '''Reads a string from the current position with c being the first character of the length (having been read already)'''
  27. length = read_int(fp, c, end = b':')
  28. s = fp.read(length)
  29. if len(s) != length:
  30. raise ValueError
  31. try:
  32. s = s.decode('utf-8')
  33. except UnicodeDecodeError:
  34. pass
  35. return s
  36. def read_int(fp, c = b'', end = b'e'):
  37. '''Reads an int from the current position with c optionally being the first digit'''
  38. i = c
  39. while True:
  40. c = fp.read(1)
  41. if c == end:
  42. break
  43. elif c in b'0123456789':
  44. i += c
  45. else:
  46. raise ValueError
  47. i = int(i.decode('ascii'))
  48. return i
  49. def read_or_stack_value(fp, stateStack, c = b''):
  50. if not c:
  51. c = fp.read(1)
  52. if c == b'l':
  53. stateStack.append(ParserState.LIST)
  54. return listEntry
  55. elif c == b'd':
  56. stateStack.append(ParserState.DICTIONARY)
  57. return dictEntry
  58. elif c == b'i':
  59. return read_int(fp)
  60. elif c in b'0123456789': # String value
  61. return read_str(fp, c)
  62. else:
  63. raise ValueError
  64. def bdecode(fp, display = False, infoWrite = None):
  65. c = fp.read(1)
  66. if c != b'd':
  67. raise ValueError
  68. stateStack = [ParserState.NONE, ParserState.DICTIONARY]
  69. inInfo = False
  70. print_ = print if display else (lambda *args, **kwargs: None)
  71. print_(f'(global): {dictEntry}')
  72. while stateStack:
  73. state = stateStack[-1]
  74. indent = ' ' * (len(stateStack) - 1)
  75. if state == ParserState.DICTIONARY:
  76. c = fp.read(1)
  77. if c == b'e': # End of dict
  78. stateStack.pop(-1)
  79. if len(stateStack) == 2 and inInfo and infoWrite:
  80. inInfo = False
  81. fp = fp.fp
  82. continue
  83. elif c in b'0123456789': # Key
  84. key = read_str(fp, c)
  85. if len(stateStack) == 2 and key == b'info' and infoWrite: # If in global dict and this is the 'info' value and a copy is desired...
  86. inInfo = True
  87. fp = CopyingFileReader(fp, infoWrite)
  88. v = read_or_stack_value(fp, stateStack)
  89. print_(f'{indent}{key!r}: {v!r}')
  90. else:
  91. raise ValueError
  92. elif state == ParserState.LIST:
  93. c = fp.read(1)
  94. if c == b'e':
  95. stateStack.pop(-1)
  96. continue
  97. else:
  98. v = read_or_stack_value(fp, stateStack, c)
  99. print_(f'{indent}- {v!r}')
  100. elif state == ParserState.NONE:
  101. assert len(stateStack) == 1
  102. return
  103. def print_torrent(fp):
  104. bdecode(fp, display = True)
  105. def get_info_hash(fp):
  106. hasher = hashlib.sha1()
  107. bdecode(fp, infoWrite = hasher.update)
  108. return hasher.hexdigest()
  109. def main():
  110. if len(sys.argv) < 3 or sys.argv[1] not in ('print', 'infohash'):
  111. print('Usage: torrent-tiny MODE FILE [FILE...]', file = sys.stderr)
  112. print('MODEs: print, infohash', file = sys.stderr)
  113. sys.exit(1)
  114. mode = sys.argv[1]
  115. for fn in sys.argv[2:]:
  116. with open(fn, 'rb') as fp:
  117. if mode == 'print':
  118. print_torrent(fp)
  119. elif mode == 'infohash':
  120. print(get_info_hash(fp))
  121. if __name__ == '__main__':
  122. main()