89d5a5ef80
B-trees with names are now working, though this required a number of
changes to the B-tree layout:
1. B-tree no-longer require name entries (LFSR_TAG_MK) on each branch.
This is a nice optimization to the design, since these name entries
just waste space in purely weight-based B-trees, which are probably
going to be most B-trees in the filesystem.
If a name entry is missing, the struct entry, which is required,
should have the effective weight of the entry.
The first entry in every rbyd block is expected to be have no name
entry, since this is the default path for B-tree lookups.
2. The first entry in every rbyd block _may_ have a name entry, which
is ignored. I'm calling these "vestigial names" to make them sound
cooler than they actually are.
These vestigial names show up in a couple complicated B-tree
operations:
- During B-tree split, since pending attributes are calculated before
the split, we need to play out pending attributes into the rbyd
before deciding what name becomes the name of entry in the parent.
This creates a vestigial name which we _could_ immediately remove,
but the remove adds additional size to the must-fit split operation
- During B-tree pop/merge, if we remove the leading no-name entry,
the second, named entry becomes the leading entry. This creates a
vestigial name that _looks_ easy enough to remove when making the
pending attributes for pop/merge, but turns out the be surprisingly
tricky if the parent undergoes a split/merge at the same time.
It may be possible to remove all these vestigial names proactively,
but this adds additional rbyd lookups to figure out the exact tag to
remove, complicates things in a fragile way, and doesn't actually
reduce storage costs until the rbyd is compacted.
The main downside is that these B-trees may be a bit more confusing
to debug.
847 lines
28 KiB
Python
Executable File
847 lines
28 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import bisect
|
|
import itertools as it
|
|
import math as m
|
|
import os
|
|
import struct
|
|
|
|
COLORS = [
|
|
'34', # blue
|
|
'31', # red
|
|
'32', # green
|
|
'35', # purple
|
|
'33', # yellow
|
|
'36', # cyan
|
|
]
|
|
|
|
|
|
def blocklim(s):
|
|
if '.' in s:
|
|
s = s.strip()
|
|
b = 10
|
|
if s.startswith('0x') or s.startswith('0X'):
|
|
s = s[2:]
|
|
b = 16
|
|
elif s.startswith('0o') or s.startswith('0O'):
|
|
s = s[2:]
|
|
b = 8
|
|
elif s.startswith('0b') or s.startswith('0B'):
|
|
s = s[2:]
|
|
b = 2
|
|
|
|
s0, s1 = s.split('.', 1)
|
|
return int(s0, b), int(s1, b)
|
|
else:
|
|
return int(s, 0)
|
|
|
|
def crc32c(data, crc=0):
|
|
crc ^= 0xffffffff
|
|
for b in data:
|
|
crc ^= b
|
|
for j in range(8):
|
|
crc = (crc >> 1) ^ ((crc & 1) * 0x82f63b78)
|
|
return 0xffffffff ^ crc
|
|
|
|
def fromleb128(data):
|
|
word = 0
|
|
for i, b in enumerate(data):
|
|
word |= ((b & 0x7f) << 7*i)
|
|
word &= 0xffffffff
|
|
if not b & 0x80:
|
|
return word, i+1
|
|
return word, len(data)
|
|
|
|
def fromtag(data):
|
|
tag, delta = fromleb128(data)
|
|
id, delta_ = fromleb128(data[delta:])
|
|
size, delta__ = fromleb128(data[delta+delta_:])
|
|
return tag&1, tag&~1, id if tag&0x8 else id-1, size, delta+delta_+delta__
|
|
|
|
def popc(x):
|
|
return bin(x).count('1')
|
|
|
|
def xxd(data, width=16, crc=False):
|
|
for i in range(0, len(data), width):
|
|
yield '%-*s %-*s' % (
|
|
3*width,
|
|
' '.join('%02x' % b for b in data[i:i+width]),
|
|
width,
|
|
''.join(
|
|
b if b >= ' ' and b <= '~' else '.'
|
|
for b in map(chr, data[i:i+width])))
|
|
|
|
def tagrepr(tag, id, size, off=None):
|
|
if (tag & ~0x3f0) == 0x0400:
|
|
return '%smk%s id%d %d' % (
|
|
'rm' if tag & 0x2 else '',
|
|
'branch' if ((tag & 0x3f0) >> 4) == 0x00
|
|
else 'reg' if ((tag & 0x3f0) >> 4) == 0x01
|
|
else 'dir' if ((tag & 0x3f0) >> 4) == 0x02
|
|
else ' 0x%02x' % ((tag & 0x3f0) >> 4),
|
|
id,
|
|
size)
|
|
elif tag == 0x0800:
|
|
return 'inlined id%d %d' % (id, size)
|
|
elif tag == 0x0810:
|
|
return 'block id%d %d' % (id, size)
|
|
elif tag == 0x0820:
|
|
return 'btree id%d %d' % (id, size)
|
|
elif tag == 0x0830:
|
|
return 'branch id%d %d' % (id, size)
|
|
elif (tag & ~0xff2) == 0x2000:
|
|
return '%suattr 0x%02x%s%s' % (
|
|
'rm' if tag & 0x2 else '',
|
|
(tag & 0xff0) >> 4,
|
|
' id%d' % id if id != -1 else '',
|
|
' %d' % size if not tag & 0x2 or size else '')
|
|
elif tag == 0x0006:
|
|
return 'grow id%d w%d' % (
|
|
id,
|
|
size)
|
|
elif tag == 0x0016:
|
|
return 'shrink id%d w%d' % (
|
|
id,
|
|
size)
|
|
elif (tag & ~0x10) == 0x0004:
|
|
return 'crc%x%s %d' % (
|
|
1 if tag & 0x10 else 0,
|
|
' 0x%02x' % id if id != -1 else '',
|
|
size)
|
|
elif tag == 0x0024:
|
|
return 'fcrc%s %d' % (
|
|
' 0x%02x' % id if id != -1 else '',
|
|
size)
|
|
elif tag & 0x8:
|
|
return 'alt%s%s 0x%x w%d %s' % (
|
|
'r' if tag & 0x2 else 'b',
|
|
'gt' if tag & 0x4 else 'le',
|
|
tag & 0x3ff0,
|
|
id,
|
|
'0x%x' % (0xffffffff & (off-size))
|
|
if off is not None
|
|
else '-%d' % off)
|
|
else:
|
|
return '0x%04x id%d %d' % (tag, id, size)
|
|
|
|
def show_log(block_size, data, rev, off, *,
|
|
color=False,
|
|
**args):
|
|
crc = crc32c(data[0:4])
|
|
|
|
# preprocess jumps
|
|
if args.get('jumps'):
|
|
jumps = []
|
|
j_ = 4
|
|
while j_ < (block_size if args.get('all') else off):
|
|
j = j_
|
|
v, tag, id, size, delta = fromtag(data[j_:])
|
|
j_ += delta
|
|
if (tag & 0xe) <= 0x4:
|
|
j_ += size
|
|
|
|
if tag & 0x8:
|
|
# figure out which alt color
|
|
if tag & 0x2:
|
|
_, ntag, _, _, _ = fromtag(data[j_:])
|
|
if ntag & 0x2:
|
|
jumps.append((j, j-size, 0, 'y'))
|
|
else:
|
|
jumps.append((j, j-size, 0, 'r'))
|
|
else:
|
|
jumps.append((j, j-size, 0, 'b'))
|
|
|
|
# figure out x-offsets to avoid collisions between jumps
|
|
for j in range(len(jumps)):
|
|
a, b, _, c = jumps[j]
|
|
x = 0
|
|
while any(
|
|
max(a, b) >= min(a_, b_)
|
|
and max(a_, b_) >= min(a, b)
|
|
and x == x_
|
|
for a_, b_, x_, _ in jumps[:j]):
|
|
x += 1
|
|
jumps[j] = a, b, x, c
|
|
|
|
def jumprepr(j):
|
|
# render jumps
|
|
chars = {}
|
|
for a, b, x, c in jumps:
|
|
c_start = (
|
|
'\x1b[33m' if color and c == 'y'
|
|
else '\x1b[31m' if color and c == 'r'
|
|
else '\x1b[90m' if color
|
|
else '')
|
|
c_stop = '\x1b[m' if color else ''
|
|
|
|
if j == a:
|
|
for x_ in range(2*x+1):
|
|
chars[x_] = '%s-%s' % (c_start, c_stop)
|
|
chars[2*x+1] = '%s\'%s' % (c_start, c_stop)
|
|
elif j == b:
|
|
for x_ in range(2*x+1):
|
|
chars[x_] = '%s-%s' % (c_start, c_stop)
|
|
chars[2*x+1] = '%s.%s' % (c_start, c_stop)
|
|
chars[0] = '%s<%s' % (c_start, c_stop)
|
|
elif j >= min(a, b) and j <= max(a, b):
|
|
chars[2*x+1] = '%s|%s' % (c_start, c_stop)
|
|
|
|
return ''.join(chars.get(x, ' ')
|
|
for x in range(max(chars.keys(), default=0)+1))
|
|
|
|
# preprocess lifetimes
|
|
lifetime_width = 0
|
|
if args.get('lifetimes'):
|
|
class Lifetime:
|
|
color_i = 0
|
|
def __init__(self, j):
|
|
self.origin = j
|
|
self.tags = set()
|
|
self.color = COLORS[self.__class__.color_i]
|
|
self.__class__.color_i = (
|
|
self.__class__.color_i + 1) % len(COLORS)
|
|
|
|
def add(self, j):
|
|
self.tags.add(j)
|
|
|
|
def __bool__(self):
|
|
return bool(self.tags)
|
|
|
|
|
|
# first figure out where each id comes from
|
|
weights = []
|
|
lifetimes = []
|
|
def index(weights, id):
|
|
for i, w in enumerate(weights):
|
|
if id < w:
|
|
return i, id
|
|
id -= w
|
|
return len(weights), 0
|
|
|
|
checkpoint_js = []
|
|
checkpoints = []
|
|
def checkpoint(j, weights, lifetimes, grows, shrinks, tags):
|
|
checkpoint_js.append(j)
|
|
checkpoints.append((
|
|
weights.copy(), lifetimes.copy(),
|
|
grows, shrinks, tags))
|
|
|
|
j_ = 4
|
|
while j_ < (block_size if args.get('all') else off):
|
|
j = j_
|
|
v, tag, id, size, delta = fromtag(data[j_:])
|
|
j_ += delta
|
|
if (tag & 0xe) <= 0x4:
|
|
j_ += size
|
|
|
|
# note we ignore out-of-bounds here for debugging
|
|
if tag == 0x0006:
|
|
# grow lifetimes
|
|
i, id_ = index(weights, id)
|
|
if id_ > 0:
|
|
weights[i:i+1] = [id_, size, weights[i]-id_]
|
|
lifetimes[i:i+1] = [lifetimes[i], Lifetime(j), lifetimes[i]]
|
|
else:
|
|
weights[i:i] = [size]
|
|
lifetimes[i:i] = [Lifetime(j)]
|
|
|
|
checkpoint(j, weights, lifetimes, {i}, set(), {i})
|
|
|
|
elif tag == 0x0016:
|
|
# shrink lifetimes
|
|
i, id_ = index(weights, id)
|
|
size_ = size
|
|
weights_ = weights.copy()
|
|
lifetimes_ = lifetimes.copy()
|
|
shrinks = set()
|
|
while size_ > 0 and i < len(weights_):
|
|
if id_ > 0:
|
|
diff = min(size_, weights_[i]-id_)
|
|
size_ -= diff
|
|
weights_[i] -= diff
|
|
i += 1
|
|
id_ = 0
|
|
elif weights_[i] > size_:
|
|
weights_[i] -= size_
|
|
size_ = 0
|
|
else:
|
|
size_ -= weights_[i]
|
|
weights_[i:i+1] = []
|
|
lifetimes_[i:i+1] = []
|
|
shrinks.add(i + len(shrinks))
|
|
|
|
checkpoint(j, weights, lifetimes, set(), shrinks, {i})
|
|
weights = weights_
|
|
lifetimes = lifetimes_
|
|
|
|
elif (tag & 0xc) == 0x0:
|
|
# attach tag to lifetime
|
|
i, id_ = index(weights, id)
|
|
if i < len(weights):
|
|
lifetimes[i].add(j)
|
|
|
|
checkpoint(j, weights, lifetimes, set(), set(), {i})
|
|
|
|
lifetime_width = 2*max((
|
|
sum(1 for lifetime in lifetimes if lifetime)
|
|
for _, lifetimes, _, _, _ in checkpoints),
|
|
default=0)
|
|
|
|
def lifetimerepr(j):
|
|
x = bisect.bisect(checkpoint_js, j)-1
|
|
j_ = checkpoint_js[x]
|
|
weights, lifetimes, grows, shrinks, tags = checkpoints[x]
|
|
|
|
reprs = []
|
|
colors = []
|
|
was = None
|
|
for i, (w, lifetime) in enumerate(zip(weights, lifetimes)):
|
|
# skip lifetimes with no tags and shrinks
|
|
if not lifetime or (j != j_ and i in shrinks):
|
|
if i in grows or i in shrinks or i in tags:
|
|
tags = tags.copy()
|
|
tags.add(i+1)
|
|
continue
|
|
|
|
if j == j_ and i in grows:
|
|
reprs.append('.')
|
|
was = 'grow'
|
|
elif j == j_ and i in shrinks:
|
|
reprs.append('\'')
|
|
was = 'shrink'
|
|
elif j == j_ and i in tags:
|
|
reprs.append('* ')
|
|
elif was == 'grow':
|
|
reprs.append('\\ ')
|
|
elif was == 'shrink':
|
|
reprs.append('/ ')
|
|
else:
|
|
reprs.append('| ')
|
|
|
|
colors.append(lifetime.color)
|
|
|
|
return '%s%*s' % (
|
|
''.join('%s%s%s' % (
|
|
'\x1b[%sm' % c if color else '',
|
|
r,
|
|
'\x1b[m' if color else '')
|
|
for r, c in zip(reprs, colors)),
|
|
lifetime_width - sum(len(r) for r in reprs), '')
|
|
|
|
# print header
|
|
print('%-8s %*s%-22s %s' % (
|
|
'off',
|
|
lifetime_width, '',
|
|
'tag',
|
|
'data (truncated)'
|
|
if not args.get('no_truncate') else ''))
|
|
|
|
# print revision count
|
|
if args.get('raw'):
|
|
print('%8s: %s' % ('%04x' % 0, next(xxd(data[0:4]))))
|
|
|
|
# print tags
|
|
j_ = 4
|
|
while j_ < (block_size if args.get('all') else off):
|
|
notes = []
|
|
|
|
j = j_
|
|
v, tag, id, size, delta = fromtag(data[j_:])
|
|
if v != (popc(crc) & 1):
|
|
notes.append('v!=%x' % (popc(crc) & 1))
|
|
tag &= ~1
|
|
crc = crc32c(data[j_:j_+delta], crc)
|
|
j_ += delta
|
|
|
|
if (tag & 0xe) <= 0x4:
|
|
if (tag & ~0x10) != 0x04:
|
|
crc = crc32c(data[j_:j_+size], crc)
|
|
# found a crc?
|
|
else:
|
|
crc_, = struct.unpack('<I', data[j_:j_+4].ljust(4, b'\0'))
|
|
if crc != crc_:
|
|
notes.append('crc!=%08x' % crc)
|
|
j_ += size
|
|
|
|
# show human-readable tag representation
|
|
print('%s%08x:%s %s%s%-57s%s%s' % (
|
|
'\x1b[90m' if color and j >= off else '',
|
|
j,
|
|
'\x1b[m' if color and j >= off else '',
|
|
lifetimerepr(j) if args.get('lifetimes') else '',
|
|
'\x1b[90m' if color and j >= off else '',
|
|
'%-22s%s' % (
|
|
tagrepr(tag, id, size, j),
|
|
' %s' % next(xxd(
|
|
data[j+delta:j+delta+min(size, 8)], 8), '')
|
|
if not args.get('no_truncate')
|
|
and (tag & 0xe) <= 0x4 else ''),
|
|
'\x1b[m' if color and j >= off else '',
|
|
' (%s)' % ', '.join(notes) if notes
|
|
else ' %s' % jumprepr(j)
|
|
if args.get('jumps')
|
|
else ''))
|
|
|
|
if args.get('raw'):
|
|
# show on-disk encoding of tags
|
|
for o, line in enumerate(xxd(data[j:j+delta])):
|
|
print('%s%8s: %s%s' % (
|
|
'\x1b[90m' if color and j >= off else '',
|
|
'%04x' % (j + o*16),
|
|
line,
|
|
'\x1b[m' if color and j >= off else ''))
|
|
|
|
# show in-device representation, including some extra
|
|
# crc/parity info
|
|
if args.get('device'):
|
|
print('%s%8s %*s%-47s %08x %x%s' % (
|
|
'\x1b[90m' if color and j >= off else '',
|
|
'',
|
|
lifetime_width, '',
|
|
'%-22s%s' % (
|
|
'%04x %08x %07x' % (tag, 0xffffffff & id, size),
|
|
' %s' % ' '.join(
|
|
'%08x' % struct.unpack('<I',
|
|
data[j+delta+i*4:j+delta+min(i*4+4,size)]
|
|
.ljust(4, b'\0'))
|
|
for i in range(min(m.ceil(size/4), 3)))[:23]
|
|
if not args.get('no_truncate')
|
|
and (tag & 0xe) <= 0x4 else ''),
|
|
crc,
|
|
popc(crc) & 1,
|
|
'\x1b[m' if color and j >= off else ''))
|
|
|
|
if (tag & 0xe) <= 0x4:
|
|
# show on-disk encoding of data
|
|
if args.get('raw') or args.get('no_truncate'):
|
|
for o, line in enumerate(xxd(data[j+delta:j+delta+size])):
|
|
print('%s%8s: %s%s' % (
|
|
'\x1b[90m' if color and j >= off else '',
|
|
'%04x' % (j+delta + o*16),
|
|
line,
|
|
'\x1b[m' if color and j >= off else ''))
|
|
|
|
|
|
def show_tree(block_size, data, rev, trunk, weight, *,
|
|
color=False,
|
|
**args):
|
|
if trunk is None:
|
|
return
|
|
|
|
# lookup a tag, returning also the search path for decoration
|
|
# purposes
|
|
def lookup(tag, id):
|
|
lower = -1
|
|
upper = weight
|
|
path = []
|
|
|
|
# descend down tree
|
|
j = trunk
|
|
while True:
|
|
_, alt, weight_, jump, delta = fromtag(data[j:])
|
|
|
|
# found an alt?
|
|
if alt & 0x8:
|
|
# follow?
|
|
if ((id, tag & ~0xf) > (upper-weight_-1, alt & ~0xf)
|
|
if alt & 0x4
|
|
else ((id, tag & ~0xf) <= (lower+weight_, alt & ~0xf))):
|
|
lower += upper-lower-1-weight_ if alt & 0x4 else 0
|
|
upper -= upper-lower-1-weight_ if not alt & 0x4 else 0
|
|
j = j - jump
|
|
|
|
if args.get('tree'):
|
|
# figure out which color
|
|
if alt & 0x2:
|
|
_, nalt, _, _, _ = fromtag(data[j+jump+delta:])
|
|
if nalt & 0x2:
|
|
path.append((j+jump, j, 'y'))
|
|
else:
|
|
path.append((j+jump, j, 'r'))
|
|
else:
|
|
path.append((j+jump, j, 'b'))
|
|
# stay on path
|
|
else:
|
|
lower += weight_ if not alt & 0x4 else 0
|
|
upper -= weight_ if alt & 0x4 else 0
|
|
j = j + delta
|
|
|
|
if args.get('tree'):
|
|
# figure out which color
|
|
if alt & 0x2:
|
|
_, nalt, _, _, _ = fromtag(data[j:])
|
|
if nalt & 0x2:
|
|
path.append((j-delta, j, 'y'))
|
|
else:
|
|
path.append((j-delta, j, 'r'))
|
|
else:
|
|
path.append((j-delta, j, 'b'))
|
|
# found tag
|
|
else:
|
|
tag_ = alt
|
|
id_ = upper-1
|
|
w_ = id_-lower
|
|
|
|
done = (id_, tag_) < (id, tag) or tag_ & 2
|
|
|
|
return done, tag_, id_, w_, j, delta, jump, path
|
|
|
|
# precompute tree
|
|
tree_width = 0
|
|
if args.get('tree'):
|
|
tags = []
|
|
paths = {}
|
|
|
|
tag, id = 0, -1
|
|
while True:
|
|
done, tag, id, w, j, delta, size, path = lookup(tag+0x10, id)
|
|
# found end of tree?
|
|
if done:
|
|
break
|
|
|
|
tags.append((j, tag, id))
|
|
for x, (a, b, c) in enumerate(path):
|
|
paths[a, b, x] = c
|
|
|
|
# align paths to nearest tag
|
|
tags.sort()
|
|
paths = {(
|
|
tags[bisect.bisect_left(tags, (a, 0, -1), hi=len(tags)-1)],
|
|
tags[bisect.bisect_left(tags, (b, 0, -1), hi=len(tags)-1)],
|
|
x): c for (a, b, x), c in paths.items()}
|
|
|
|
# also find the maximum depth
|
|
depth = max((x+1 for _, _, x in paths.keys()), default=0)
|
|
if depth > 0:
|
|
tree_width = 2*depth + 2
|
|
|
|
def treerepr(j):
|
|
if depth == 0:
|
|
return ''
|
|
|
|
_, tag, id = tags[bisect.bisect_left(
|
|
tags, (j, 0, -1), hi=len(tags)-1)]
|
|
|
|
def c_start(c):
|
|
return ('\x1b[33m' if color and c == 'y'
|
|
else '\x1b[31m' if color and c == 'r'
|
|
else '\x1b[90m' if color
|
|
else '')
|
|
|
|
def c_stop(c):
|
|
return '\x1b[m' if color else ''
|
|
|
|
path = []
|
|
seen = None
|
|
for x in range(depth):
|
|
if any(x == x_ and tag == a_tag and id == a_id
|
|
for (_, a_tag, a_id), _, x_ in paths.keys()):
|
|
c = next(c
|
|
for ((_, a_tag, a_id), _, x_), c in paths.items()
|
|
if x == x_ and tag == a_tag and id == a_id)
|
|
path.append('%s+%s' % (c_start(c), c_stop(c)))
|
|
elif any(x == x_ and tag == b_tag and id == b_id
|
|
for _, (_, b_tag, b_id), x_ in paths.keys()):
|
|
a_tag, a_id, c = next((a_tag, a_id, c)
|
|
for ((_, a_tag, a_id), (_, b_tag, b_id), x_), c
|
|
in paths.items()
|
|
if x == x_ and tag == b_tag and id == b_id)
|
|
if (a_id, a_tag) < (id, tag):
|
|
path.append('%s\'%s' % (c_start(c), c_stop(c)))
|
|
else:
|
|
path.append('%s.%s' % (c_start(c), c_stop(c)))
|
|
elif any(x == x_
|
|
and (id, tag) >= min((a_id, a_tag), (b_id, b_tag))
|
|
and (id, tag) <= max((a_id, a_tag), (b_id, b_tag))
|
|
for (_, a_tag, a_id), (_, b_tag, b_id), x_
|
|
in paths.keys()):
|
|
c = next(c
|
|
for ((_, a_tag, a_id), (_, b_tag, b_id), x_), c
|
|
in paths.items()
|
|
if x == x_
|
|
and (id, tag) >= min((a_id, a_tag), (b_id, b_tag))
|
|
and (id, tag) <= max((a_id, a_tag), (b_id, b_tag)))
|
|
path.append('%s|%s' % (c_start(c), c_stop(c)))
|
|
elif seen:
|
|
path.append('%s-%s' % (c_start(seen), c_stop(seen)))
|
|
else:
|
|
path.append(' ')
|
|
|
|
if any(x == x_ and tag == b_tag and id == b_id
|
|
for _, (_, b_tag, b_id), x_ in paths.keys()):
|
|
c = next(c
|
|
for (_, (_, b_tag, b_id), x_), c in paths.items()
|
|
if x == x_ and tag == b_tag and id == b_id)
|
|
seen = c
|
|
|
|
if seen and x == depth-1:
|
|
path.append('%s->%s' % (c_start(seen), c_stop(seen)))
|
|
elif seen:
|
|
path.append('%s-%s' % (c_start(seen), c_stop(seen)))
|
|
else:
|
|
path.append(' ')
|
|
|
|
return ' %s' % ''.join(path)
|
|
|
|
# print header
|
|
w_width = 2*m.ceil(m.log10(max(1, weight)+1))+1
|
|
print('%-8s %*s%-*s %-22s %s' % (
|
|
'off',
|
|
tree_width, '',
|
|
w_width, 'ids',
|
|
'tag',
|
|
'data (truncated)'
|
|
if not args.get('no_truncate') else ''))
|
|
|
|
tag, id = 0, -1
|
|
while True:
|
|
done, tag, id, w, j, delta, size, path = lookup(tag+0x10, id)
|
|
# found end of tree?
|
|
if done:
|
|
break
|
|
|
|
# show human-readable tag representation
|
|
print('%08x:%s %*s %-57s' % (
|
|
j,
|
|
treerepr(j) if args.get('tree') else '',
|
|
w_width, '%d-%d' % (id-(w-1), id)
|
|
if w > 1 else id
|
|
if w > 0 else '',
|
|
'%-22s%s' % (
|
|
tagrepr(tag, id, size, j),
|
|
' %s' % next(xxd(
|
|
data[j+delta:j+delta+min(size, 8)], 8), '')
|
|
if not args.get('no_truncate')
|
|
and (tag & 0xe) <= 0x4 else '')))
|
|
|
|
# show in-device representation
|
|
if args.get('device'):
|
|
print('%8s %*s%*s %s' % (
|
|
'',
|
|
tree_width, '',
|
|
w_width, '',
|
|
'%-22s%s' % (
|
|
'%04x %08x %07x' % (tag, 0xffffffff & id, size),
|
|
' %s' % ' '.join(
|
|
'%08x' % struct.unpack('<I',
|
|
data[j+delta+i*4:j+delta+min(i*4+4,size)]
|
|
.ljust(4, b'\0'))
|
|
for i in range(min(m.ceil(size/4), 3)))[:23]
|
|
if not args.get('no_truncate')
|
|
and (tag & 0xe) <= 0x4 else '')))
|
|
|
|
if args.get('raw'):
|
|
# show on-disk encoding of tags
|
|
for o, line in enumerate(xxd(data[j:j+delta])):
|
|
print('%8s: %s' % (
|
|
'%04x' % (j + o*16),
|
|
line))
|
|
|
|
if (tag & 0xe) <= 0x4:
|
|
# show on-disk encoding of data
|
|
if args.get('raw') or args.get('no_truncate'):
|
|
for o, line in enumerate(xxd(data[j+delta:j+delta+size])):
|
|
print('%8s: %s' % (
|
|
'%04x' % (j+delta + o*16),
|
|
line))
|
|
|
|
|
|
def main(disk, block_size=None, block1=0, block2=None, *,
|
|
limit=None,
|
|
trunk=None,
|
|
color='auto',
|
|
**args):
|
|
# figure out what color should be
|
|
if color == 'auto':
|
|
color = sys.stdout.isatty()
|
|
elif color == 'always':
|
|
color = True
|
|
else:
|
|
color = False
|
|
|
|
with open(disk, 'rb') as f:
|
|
# if block_size is omitted, assume the block device is one big block
|
|
if block_size is None:
|
|
f.seek(0, os.SEEK_END)
|
|
block_size = f.tell()
|
|
|
|
# blocks may also encode limits
|
|
blocks = [
|
|
block[0] if isinstance(block, tuple) else block
|
|
for block in [block1, block2]
|
|
if block is not None]
|
|
limits = [
|
|
limit if limit is not None
|
|
else block[1] if isinstance(block, tuple)
|
|
else None
|
|
for block in [block1, block2]
|
|
if block is not None]
|
|
|
|
# read each block
|
|
datas = []
|
|
for block, limit in zip(blocks, limits):
|
|
f.seek(block * block_size)
|
|
datas.append(f.read(limit if limit is not None else block_size))
|
|
|
|
# first figure out which block as the most recent revision
|
|
def fetch(data):
|
|
rev, = struct.unpack('<I', data[0:4].ljust(4, b'\0'))
|
|
crc = crc32c(data[0:4])
|
|
off = 0
|
|
j_ = 4
|
|
trunk = None
|
|
trunk_ = None
|
|
weight = 0
|
|
weight_ = 0
|
|
wastrunk = False
|
|
while j_ < len(data):
|
|
v, tag, id, size, delta = fromtag(data[j_:])
|
|
if v != (popc(crc) & 1):
|
|
break
|
|
crc = crc32c(data[j_:j_+delta], crc)
|
|
j_ += delta
|
|
|
|
# find trunk
|
|
if not wastrunk and (tag & 0xe) != 0x4:
|
|
trunk_ = j_ - delta
|
|
wastrunk = not not tag & 0x8
|
|
|
|
# keep track of weight
|
|
if tag == 0x0006:
|
|
weight_ += size
|
|
elif tag == 0x0016:
|
|
weight_ = max(weight_ - size, 0)
|
|
|
|
# take care of crcs
|
|
if (tag & 0xe) <= 0x4:
|
|
if (tag & ~0x10) != 0x04:
|
|
crc = crc32c(data[j_:j_+size], crc)
|
|
# found a crc?
|
|
else:
|
|
crc_, = struct.unpack('<I', data[j_:j_+4].ljust(4, b'\0'))
|
|
if crc != crc_:
|
|
break
|
|
# commit what we have
|
|
off = j_ + size
|
|
trunk = trunk_
|
|
weight = weight_
|
|
j_ += size
|
|
|
|
return rev, off, trunk, weight
|
|
|
|
revs, offs, trunks, weights = [], [], [], []
|
|
i = 0
|
|
for data in datas:
|
|
rev, off, trunk_, weight = fetch(data)
|
|
revs.append(rev)
|
|
offs.append(off)
|
|
trunks.append(trunk_)
|
|
weights.append(weight)
|
|
|
|
# compare with sequence arithmetic
|
|
if off and ((rev - revs[i]) & 0x80000000):
|
|
i = len(revs)-1
|
|
|
|
# print contents of the winning metadata block
|
|
block, limit, data, rev, off, trunk, weight = (
|
|
blocks[i], limits[i], datas[i], revs[i], offs[i],
|
|
trunk if trunk is not None else trunks[i],
|
|
weights[i])
|
|
|
|
print('rbyd 0x%x%s, rev %d, size %d, weight %d%s' % (
|
|
block, '.%x' % limit if limit is not None else '',
|
|
rev, off, weight,
|
|
' (was 0x%x%s, %d, %d, %d)' % (
|
|
blocks[~i], '.%x' % limits[~i] if limits[~i] is not None else '',
|
|
revs[~i], offs[~i], weights[~i])
|
|
if len(blocks) > 1 else ''))
|
|
|
|
if args.get('log'):
|
|
show_log(block_size, data, rev, off,
|
|
color=color,
|
|
**args)
|
|
else:
|
|
show_tree(block_size, data, rev, trunk, weight,
|
|
color=color,
|
|
**args)
|
|
|
|
if args.get('error_on_corrupt') and off == 0:
|
|
sys.exit(2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
import sys
|
|
parser = argparse.ArgumentParser(
|
|
description="Debug rbyd metadata.",
|
|
allow_abbrev=False)
|
|
parser.add_argument(
|
|
'disk',
|
|
help="File containing the block device.")
|
|
parser.add_argument(
|
|
'block1',
|
|
nargs='?',
|
|
type=blocklim,
|
|
help="Block address of the first metadata block.")
|
|
parser.add_argument(
|
|
'block2',
|
|
nargs='?',
|
|
type=blocklim,
|
|
help="Block address of the second metadata block.")
|
|
parser.add_argument(
|
|
'-B', '--block-size',
|
|
type=lambda x: int(x, 0),
|
|
help="Block size in bytes.")
|
|
parser.add_argument(
|
|
'-L', '--limit',
|
|
type=lambda x: int(x, 0),
|
|
help="Use this offset as the rbyd limit.")
|
|
parser.add_argument(
|
|
'--trunk',
|
|
type=lambda x: int(x, 0),
|
|
help="Use this offset as the trunk of the tree.")
|
|
parser.add_argument(
|
|
'--color',
|
|
choices=['never', 'always', 'auto'],
|
|
default='auto',
|
|
help="When to use terminal colors. Defaults to 'auto'.")
|
|
parser.add_argument(
|
|
'-a', '--all',
|
|
action='store_true',
|
|
help="Don't stop parsing on bad commits.")
|
|
parser.add_argument(
|
|
'-l', '--log',
|
|
action='store_true',
|
|
help="Show the raw tags as they appear in the log.")
|
|
parser.add_argument(
|
|
'-r', '--raw',
|
|
action='store_true',
|
|
help="Show the raw data including tag encodings.")
|
|
parser.add_argument(
|
|
'-x', '--device',
|
|
action='store_true',
|
|
help="Show the device-side representation of tags.")
|
|
parser.add_argument(
|
|
'-T', '--no-truncate',
|
|
action='store_true',
|
|
help="Don't truncate, show the full contents.")
|
|
parser.add_argument(
|
|
'-t', '--tree',
|
|
action='store_true',
|
|
help="Show the rbyd tree.")
|
|
parser.add_argument(
|
|
'-j', '--jumps',
|
|
action='store_true',
|
|
help="Show alt pointer jumps in the margin.")
|
|
parser.add_argument(
|
|
'-g', '--lifetimes',
|
|
action='store_true',
|
|
help="Show inserts/deletes of ids in the margin.")
|
|
parser.add_argument(
|
|
'-e', '--error-on-corrupt',
|
|
action='store_true',
|
|
help="Error if no valid commit is found.")
|
|
sys.exit(main(**{k: v
|
|
for k, v in vars(parser.parse_intermixed_args()).items()
|
|
if v is not None}))
|