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