readtree.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. #!/usr/bin/env python3
  2. import struct
  3. import sys
  4. import json
  5. import io
  6. import itertools as it
  7. from readmdir import Tag, MetadataPair
  8. def main(args):
  9. superblock = None
  10. gstate = b'\0\0\0\0\0\0\0\0\0\0\0\0'
  11. dirs = []
  12. mdirs = []
  13. corrupted = []
  14. cycle = False
  15. with open(args.disk, 'rb') as f:
  16. tail = (args.block1, args.block2)
  17. hard = False
  18. while True:
  19. for m in it.chain((m for d in dirs for m in d), mdirs):
  20. if set(m.blocks) == set(tail):
  21. # cycle detected
  22. cycle = m.blocks
  23. if cycle:
  24. break
  25. # load mdir
  26. data = []
  27. blocks = {}
  28. for block in tail:
  29. f.seek(block * args.block_size)
  30. data.append(f.read(args.block_size)
  31. .ljust(args.block_size, b'\xff'))
  32. blocks[id(data[-1])] = block
  33. mdir = MetadataPair(data)
  34. mdir.blocks = tuple(blocks[id(p.data)] for p in mdir.pair)
  35. # fetch some key metadata as a we scan
  36. try:
  37. mdir.tail = mdir[Tag('tail', 0, 0)]
  38. if mdir.tail.size != 8 or mdir.tail.data == 8*b'\xff':
  39. mdir.tail = None
  40. except KeyError:
  41. mdir.tail = None
  42. # have superblock?
  43. try:
  44. nsuperblock = mdir[
  45. Tag(0x7ff, 0x3ff, 0), Tag('superblock', 0, 0)]
  46. superblock = nsuperblock, mdir[Tag('inlinestruct', 0, 0)]
  47. except KeyError:
  48. pass
  49. # have gstate?
  50. try:
  51. ngstate = mdir[Tag('movestate', 0, 0)]
  52. gstate = bytes((a or 0) ^ (b or 0)
  53. for a,b in it.zip_longest(gstate, ngstate.data))
  54. except KeyError:
  55. pass
  56. # corrupted?
  57. if not mdir:
  58. corrupted.append(mdir)
  59. # add to directories
  60. mdirs.append(mdir)
  61. if mdir.tail is None or not mdir.tail.is_('hardtail'):
  62. dirs.append(mdirs)
  63. mdirs = []
  64. if mdir.tail is None:
  65. break
  66. tail = struct.unpack('<II', mdir.tail.data)
  67. hard = mdir.tail.is_('hardtail')
  68. # find paths
  69. dirtable = {}
  70. for dir in dirs:
  71. dirtable[frozenset(dir[0].blocks)] = dir
  72. pending = [("/", dirs[0])]
  73. while pending:
  74. path, dir = pending.pop(0)
  75. for mdir in dir:
  76. for tag in mdir.tags:
  77. if tag.is_('dir'):
  78. try:
  79. npath = tag.data.decode('utf8')
  80. dirstruct = mdir[Tag('dirstruct', tag.id, 0)]
  81. nblocks = struct.unpack('<II', dirstruct.data)
  82. nmdir = dirtable[frozenset(nblocks)]
  83. pending.append(((path + '/' + npath), nmdir))
  84. except KeyError:
  85. pass
  86. dir[0].path = path.replace('//', '/')
  87. # print littlefs + version info
  88. version = ('?', '?')
  89. if superblock:
  90. version = tuple(reversed(
  91. struct.unpack('<HH', superblock[1].data[0:4].ljust(4, b'\xff'))))
  92. print("%-47s%s" % ("littlefs v%s.%s" % version,
  93. "data (truncated, if it fits)"
  94. if not any([args.no_truncate, args.tags, args.log, args.all]) else ""))
  95. # print gstate
  96. print("gstate 0x%s" % ''.join('%02x' % c for c in gstate))
  97. tag = Tag(struct.unpack('<I', gstate[0:4].ljust(4, b'\xff'))[0])
  98. blocks = struct.unpack('<II', gstate[4:4+8].ljust(8, b'\xff'))
  99. if tag.size or not tag.isvalid:
  100. print(" orphans >=%d" % max(tag.size, 1))
  101. if tag.type:
  102. print(" move dir {%#x, %#x} id %d" % (
  103. blocks[0], blocks[1], tag.id))
  104. # print mdir info
  105. for i, dir in enumerate(dirs):
  106. print("dir %s" % (json.dumps(dir[0].path)
  107. if hasattr(dir[0], 'path') else '(orphan)'))
  108. for j, mdir in enumerate(dir):
  109. print("mdir {%#x, %#x} rev %d (was %d)%s%s" % (
  110. mdir.blocks[0], mdir.blocks[1], mdir.rev, mdir.pair[1].rev,
  111. ' (corrupted!)' if not mdir else '',
  112. ' -> {%#x, %#x}' % struct.unpack('<II', mdir.tail.data)
  113. if mdir.tail else ''))
  114. f = io.StringIO()
  115. if args.log:
  116. mdir.dump_log(f, truncate=not args.no_truncate)
  117. elif args.all:
  118. mdir.dump_all(f, truncate=not args.no_truncate)
  119. else:
  120. mdir.dump_tags(f, truncate=not args.no_truncate)
  121. lines = list(filter(None, f.getvalue().split('\n')))
  122. for k, line in enumerate(lines):
  123. print("%s %s" % (
  124. ' ' if j == len(dir)-1 else
  125. 'v' if k == len(lines)-1 else
  126. '|',
  127. line))
  128. errcode = 0
  129. for mdir in corrupted:
  130. errcode = errcode or 1
  131. print("*** corrupted mdir {%#x, %#x}! ***" % (
  132. mdir.blocks[0], mdir.blocks[1]))
  133. if cycle:
  134. errcode = errcode or 2
  135. print("*** cycle detected {%#x, %#x}! ***" % (
  136. cycle[0], cycle[1]))
  137. return errcode
  138. if __name__ == "__main__":
  139. import argparse
  140. import sys
  141. parser = argparse.ArgumentParser(
  142. description="Dump semantic info about the metadata tree in littlefs")
  143. parser.add_argument('disk',
  144. help="File representing the block device.")
  145. parser.add_argument('block_size', type=lambda x: int(x, 0),
  146. help="Size of a block in bytes.")
  147. parser.add_argument('block1', nargs='?', default=0,
  148. type=lambda x: int(x, 0),
  149. help="Optional first block address for finding the superblock.")
  150. parser.add_argument('block2', nargs='?', default=1,
  151. type=lambda x: int(x, 0),
  152. help="Optional second block address for finding the superblock.")
  153. parser.add_argument('-l', '--log', action='store_true',
  154. help="Show tags in log.")
  155. parser.add_argument('-a', '--all', action='store_true',
  156. help="Show all tags in log, included tags in corrupted commits.")
  157. parser.add_argument('-T', '--no-truncate', action='store_true',
  158. help="Show the full contents of files/attrs/tags.")
  159. sys.exit(main(parser.parse_args()))