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.
 
 
 

180 lines
4.0 KiB

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