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