The little things give you away... A collection of various small helper stuff
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 

180 lignes
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()