scripts: Reworked dbgbtree.py, adopted Btree class

Like the Rbyd class, Btree serves as an abstraction for littlefs's
btrees in Python.

New classes:

- Btree - btree abstraction, note this does _not_ inherit from Rbyd. I
  find that sort of inheritance too error-prone. Instead Btree
  _contains_ the root rbyd, which can always be accessed via Btree.rbyd.

  If you want low-level root-rbyd details, just access Btree.rbyd.

  Though most fields that are relevant to the Btree are also forwarded
  via Python's @property properties.

- Bd - This just serves as a handle for the disk file that includes
  block_size/block_count metadata.

One important change to note is the adoption of required vestigial names
in all btree nodes (yes this scripts was written... checks notes...
2 years ago... even the same month huh). This means we don't need the
parent name mapping, so the non-inner btree printing code no longer
needs to be extremely confusing at all times.

Also adopted the Rbyd class and friends, and backported Bd to
dbgrbyd.py.

Also tried to give a couple useful algorithms their own self-contained
functions, mainly:

- pathdelta - for emulating a traversal over exhaustive paths

- treerepr - for the common ascii tree rendering code
This commit is contained in:
Christopher Haster
2025-03-20 03:49:21 -05:00
parent 73127470f9
commit 46c55722a5
2 changed files with 1084 additions and 601 deletions
+868 -474
View File
File diff suppressed because it is too large Load Diff
+205 -116
View File
@@ -265,6 +265,108 @@ def tagrepr(tag, weight=None, size=None, off=None):
' w%d' % weight if weight is not None else '', ' w%d' % weight if weight is not None else '',
' %d' % size if size is not None else '') ' %d' % size if size is not None else '')
# tree branches are an abstract thing for tree rendering
class TreeBranch:
def __init__(self, a, b, depth=0, color='b'):
# a and b are context specific
self.a = a
self.b = b
self.depth = depth
self.color = color
def __repr__(self):
return '%s(%s, %s, %s, %s)' % (
self.__class__.__name__,
self.a,
self.b,
self.depth,
self.color)
def __eq__(self, other):
return ((self.a, self.b, self.depth, self.color)
== (other.a, other.b, other.depth, other.color))
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash((self.a, self.b, self.depth))
def treerepr(tree, x, depth=None, color=False):
# find the max depth from the tree
if depth is None:
depth = max((t.depth+1 for t in tree), default=0)
if depth == 0:
return ''
def branchrepr(tree, x, d, was):
for t in tree:
if t.depth == d and t.b == x:
if any(t.depth == d and t.a == x
for t in tree):
return '+-', t.color, t.color
elif any(t.depth == d
and x > min(t.a, t.b)
and x < max(t.a, t.b)
for t in tree):
return '|-', t.color, t.color
elif t.a < t.b:
return '\'-', t.color, t.color
else:
return '.-', t.color, t.color
for t in tree:
if t.depth == d and t.a == x:
return '+ ', t.color, None
for t in tree:
if (t.depth == d
and x > min(t.a, t.b)
and x < max(t.a, t.b)):
return '| ', t.color, was
if was:
return '--', was, was
return ' ', None, None
trunk = []
was = None
for d in range(depth):
t, c, was = branchrepr(tree, x, d, was)
trunk.append('%s%s%s%s' % (
'\x1b[33m' if color and c == 'y'
else '\x1b[31m' if color and c == 'r'
else '\x1b[90m' if color and c == 'b'
else '',
t,
('>' if was else ' ') if d == depth-1 else '',
'\x1b[m' if color and c else ''))
return '%s ' % ''.join(trunk)
# a simple wrapper over an open file with bd geometry
class Bd:
def __init__(self, f, block_size=None, block_count=None):
self.f = f
self.block_size = block_size
self.block_count = block_count
def __repr__(self):
return '<%s %sx%s>' % (
self.__class__.__name__,
self.block_size,
self.block_count)
def read(self, size=-1):
return self.f.read(size)
def seek(self, block, off, whence=0):
pos = self.f.seek(block*self.block_size + off, whence)
return pos // block_size, pos % block_size
def readblock(self, block):
self.f.seek(block*self.block_size)
return self.f.read(self.block_size)
# tagged data in an rbyd # tagged data in an rbyd
class Rattr: class Rattr:
def __init__(self, tag, weight, block, toff, off, data): def __init__(self, tag, weight, block, toff, off, data):
@@ -297,6 +399,16 @@ class Rattr:
def __iter__(self): def __iter__(self):
return iter(self.data) return iter(self.data)
def __eq__(self, other):
return ((self.tag, self.weight, self.data)
== (other.tag, other.weight, other.data))
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash((self.tag, self.weight, self.data))
class Ralt: class Ralt:
def __init__(self, tag, weight, block, toff, off, jump, color=None): def __init__(self, tag, weight, block, toff, off, jump, color=None):
self.tag = tag self.tag = tag
@@ -321,22 +433,16 @@ class Ralt:
def tagrepr(self): def tagrepr(self):
return tagrepr(self.tag, self.weight, self.jump, self.toff) return tagrepr(self.tag, self.weight, self.jump, self.toff)
# tree branches are an abstract thing for tree rendering def __eq__(self, other):
class TreeBranch: return ((self.tag, self.weight, self.jump)
def __init__(self, a, b, depth, color): == (other.tag, other.weight, other.jump))
# note a and b are context specific
self.a = a def __ne__(self, other):
self.b = b return not self.__eq__(other)
self.depth = depth
self.color = color def __hash__(self):
return hash((self.tag, self.weight, self.jump))
def __repr__(self):
return '%s(%s, %s, %s, %s)' % (
self.__class__.__name__,
self.a,
self.b,
self.depth,
self.color)
# our core rbyd type # our core rbyd type
class Rbyd: class Rbyd:
@@ -369,26 +475,30 @@ class Rbyd:
self.trunk) self.trunk)
def __repr__(self): def __repr__(self):
return '<%s %s>' % (self.__class__.__name__, self.addr()) return '<%s %s w%s>' % (
self.__class__.__name__,
self.addr(),
self.weight)
def __bool__(self): def __bool__(self):
return not self.corrupt return not self.corrupt
def __eq__(self, other): def __eq__(self, other):
return (self.blocks, self.trunk) == (other.blocks, other.trunk) return ((frozenset(self.blocks), self.trunk)
== (frozenset(other.blocks), other.trunk))
def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(other) return not self.__eq__(other)
def __hash__(self): def __hash__(self):
return hash((self.blocks, self.trunk)) return hash((frozenset(self.blocks), self.trunk))
@classmethod @classmethod
def fetch(cls, data, block, trunk=None, cksum=None): def fetch(cls, bd, blocks, trunk=None, cksum=None):
# multiple blocks? unfortunately this must be a list # multiple blocks? unfortunately this must be a list
if isinstance(block, list): if isinstance(blocks, list):
# fetch all blocks # fetch all blocks
rbyds = [cls.fetch(data, block, trunk, cksum) for block in block] rbyds = [cls.fetch(bd, block, trunk, cksum) for block in blocks]
# determine most recent revision # determine most recent revision
i = 0 i = 0
for i_, rbyd in enumerate(rbyds): for i_, rbyd in enumerate(rbyds):
@@ -406,7 +516,9 @@ class Rbyd:
for j in range(len(rbyds)-1)) for j in range(len(rbyds)-1))
return rbyd return rbyd
# block may encode a trunk block = blocks
# blocks may also encode trunks
block, trunk = ( block, trunk = (
block[0] if isinstance(block, tuple) block[0] if isinstance(block, tuple)
else block, else block,
@@ -414,15 +526,15 @@ class Rbyd:
else block[1] if isinstance(block, tuple) else block[1] if isinstance(block, tuple)
else None) else None)
# data can be either disk + block_size tuple or data # bd can be either a bd reference or preread data
# #
# note preread data can be useful for avoiding race conditions # preread data can be useful for avoiding race conditions
# with cksums and shrubs # with cksums and shrubs
if isinstance(data, tuple): if isinstance(bd, Bd):
f, block_size, *_ = data # seek/read the block
# seek to the block data = bd.readblock(block)
f.seek(block * block_size) else:
data = f.read(block_size) data = bd
# fetch the rbyd # fetch the rbyd
rev = fromle32(data[0:4]) rev = fromle32(data[0:4])
@@ -517,17 +629,17 @@ class Rbyd:
# cksum mismatch? # cksum mismatch?
if cksum is not None and cksum_ != cksum: if cksum is not None and cksum_ != cksum:
return cls(data, block, trunk or 0, 0, rev, 0, cksum_, return cls(data, block, 0, 0, rev, 0, cksum_,
corrupt=True) corrupt=True)
return cls(data, block, trunk_, weight, rev, eoff, cksum_, return cls(data, block, trunk_, weight, rev, eoff, cksum_,
gcksumdelta=gcksumdelta, gcksumdelta=gcksumdelta,
corrupt=not trunk_) corrupt=not trunk_)
def lookupnext(self, rid, tag=None, *, def lookupnext(self, rid=-1, tag=None, *,
path=False): path=False):
if not self: if not self:
return None, None, None, *(([],) if path else ()) return None, None, None, None, *(([],) if path else ())
tag = max(tag or 0, 0x1) tag = max(tag or 0, 0x1)
lower = 0 lower = 0
@@ -595,9 +707,11 @@ class Rbyd:
w_ = upper-lower w_ = upper-lower
if not tag_ or (rid_, tag_) < (rid, tag): if not tag_ or (rid_, tag_) < (rid, tag):
return None, None, None, *(([],) if path else ()) return None, None, None, None, *(([],) if path else ())
return (rid_, tag_, return (rid_,
tag_,
w_,
Rattr(tag_, w_, self.block, j, j+d, Rattr(tag_, w_, self.block, j, j+d,
self.data[j+d:j+d+jump]), self.data[j+d:j+d+jump]),
*((path_,) if path else ())) *((path_,) if path else ()))
@@ -607,7 +721,8 @@ class Rbyd:
if tag is None: if tag is None:
tag, mask = 0, 0xffff tag, mask = 0, 0xffff
rid_, tag_, rattr_, *path_ = self.lookupnext(rid, tag & ~(mask or 0), rid_, tag_, w_, rattr_, *path_ = self.lookupnext(
rid, tag & ~(mask or 0),
path=path) path=path)
if (rid_ is None if (rid_ is None
or rid_ != rid or rid_ != rid
@@ -642,29 +757,59 @@ class Rbyd:
else: else:
return v is not None return v is not None
def __iter__(self): def rids(self, *,
rid, tag = -1, 0 path=False):
rid = -1
while True: while True:
rid, tag, rattr = self.lookupnext(rid, tag+0x1) rid, tag, w, rattr, *path_ = self.lookupnext(rid,
path=path)
# found end of tree? # found end of tree?
if rid is None: if rid is None:
break break
yield rid, tag, rattr yield rid, tag, w, rattr, *path_
rid += 1
def rattrs(self, rid=None, *,
path=False):
if rid is None:
rid, tag = -1, 0
while True:
rid, tag, w, rattr, *path_ = self.lookupnext(rid, tag+0x1,
path=path)
# found end of tree?
if rid is None:
break
yield rid, tag, w, rattr, *path_
else:
tag = 0
while True:
rid_, tag, w, rattr, *path_ = self.lookupnext(rid, tag+0x1,
path=path)
# found end of tree?
if rid_ is None or rid_ != rid:
break
yield tag, rattr, *path_
def __iter__(self):
return self.rattrs()
# lookup by name # lookup by name
def namelookup(self, did, name): def namelookup(self, did, name):
# binary search # binary search
best = (False, None, None, None) best = (False, None, None, None, None)
lower = 0 lower = 0
upper = self.weight upper = self.weight
while lower < upper: while lower < upper:
rid, tag, rattr = self.lookupnext(lower + (upper-1-lower)//2) rid, tag, w, rattr = self.lookupnext(
lower + (upper-1-lower)//2)
if rid is None: if rid is None:
break break
# treat vestigial names as a catch-all # treat vestigial names as a catch-all
if ((tag == TAG_NAME and rid-(rattr.weight-1) == 0) if ((tag == TAG_NAME and rid-(w-1) == 0)
or (tag & 0xff00) != TAG_NAME): or (tag & 0xff00) != TAG_NAME):
did_ = 0 did_ = 0
name_ = b'' name_ = b''
@@ -674,31 +819,23 @@ class Rbyd:
# bisect search space # bisect search space
if (did_, name_) > (did, name): if (did_, name_) > (did, name):
upper = rid-(rattr.weight-1) upper = rid-(w-1)
elif (did_, name_) < (did, name): elif (did_, name_) < (did, name):
lower = rid + 1 lower = rid + 1
# keep track of best match # keep track of best match
best = (False, rid, tag, rattr) best = (False, rid, tag, w, rattr)
else: else:
# found a match # found a match
return True, rid, tag, rattr return True, rid, tag, w, rattr
return best return best
# create tree representation for debugging # create tree representation for debugging
def tree(self, *, def tree(self, **args):
rbyd=False):
trunks = co.defaultdict(lambda: (-1, 0)) trunks = co.defaultdict(lambda: (-1, 0))
alts = co.defaultdict(lambda: {}) alts = co.defaultdict(lambda: {})
rid, tag = -1, 0 for rid, tag, w, rattr, path in self.rattrs(path=True):
while True:
rid, tag, rattr, path = self.lookupnext(rid, tag+0x1,
path=True)
# found end of tree?
if rid is None:
break
# keep track of trunks/alts # keep track of trunks/alts
trunks[rattr.toff] = (rid, tag) trunks[rattr.toff] = (rid, tag)
@@ -708,7 +845,7 @@ class Rbyd:
else: else:
alts[ralt.toff] |= {'nf': ralt.off, 'c': ralt.color} alts[ralt.toff] |= {'nf': ralt.off, 'c': ralt.color}
if rbyd: if args.get('tree_rbyd'):
# treat unreachable alts as converging paths # treat unreachable alts as converging paths
for j_, alt in alts.items(): for j_, alt in alts.items():
if 'f' not in alt: if 'f' not in alt:
@@ -772,14 +909,14 @@ class Rbyd:
tree.add(TreeBranch( tree.add(TreeBranch(
alt['nft'], alt['nft'],
alt['nft'], alt['nft'],
depth=t_depth-1 - alt['h'], t_depth-1 - alt['h'],
color=alt['c'])) alt['c']))
if alt['ft'] != alt['nft']: if alt['ft'] != alt['nft']:
tree.add(TreeBranch( tree.add(TreeBranch(
alt['nft'], alt['nft'],
alt['ft'], alt['ft'],
depth=t_depth-1 - alt['h'], t_depth-1 - alt['h'],
color='b')) 'b'))
return tree return tree
@@ -1164,74 +1301,25 @@ def dbg_tree(rbyd, *,
# precompute tree # precompute tree
t_width = 0 t_width = 0
if args.get('tree') or args.get('tree_rbyd'): if args.get('tree') or args.get('tree_rbyd'):
tree = rbyd.tree(rbyd=args.get('tree_rbyd')) tree = rbyd.tree(**args)
# find the max depth from the tree # find the max depth from the tree
t_depth = max((b.depth+1 for b in tree), default=0) t_depth = max((t.depth+1 for t in tree), default=0)
if t_depth > 0: if t_depth > 0:
t_width = 2*t_depth + 2 t_width = 2*t_depth + 2
def treerepr(rid, tag):
if t_depth == 0:
return ''
def branchrepr(x, d, was):
for b in tree:
if b.depth == d and b.b == x:
if any(b.depth == d and b.a == x
for b in tree):
return '+-', b.color, b.color
elif any(b.depth == d
and x > min(b.a, b.b)
and x < max(b.a, b.b)
for b in tree):
return '|-', b.color, b.color
elif b.a < b.b:
return '\'-', b.color, b.color
else:
return '.-', b.color, b.color
for b in tree:
if b.depth == d and b.a == x:
return '+ ', b.color, None
for b in tree:
if (b.depth == d
and x > min(b.a, b.b)
and x < max(b.a, b.b)):
return '| ', b.color, was
if was:
return '--', was, was
return ' ', None, None
trunk = []
was = None
for d in range(t_depth):
t, c, was = branchrepr((rid, tag), d, was)
trunk.append('%s%s%s%s' % (
'\x1b[33m' if color and c == 'y'
else '\x1b[31m' if color and c == 'r'
else '\x1b[90m' if color and c == 'b'
else '',
t,
('>' if was else ' ') if d == t_depth-1 else '',
'\x1b[m' if color and c else ''))
return '%s ' % ''.join(trunk)
# dynamically size the id field # dynamically size the id field
w_width = mt.ceil(mt.log10(max(1, rbyd.weight)+1)) w_width = mt.ceil(mt.log10(max(1, rbyd.weight)+1))
for i, (rid, tag, rattr) in enumerate(rbyd): for i, (rid, tag, w, rattr) in enumerate(rbyd):
# show human-readable tag representation # show human-readable tag representation
print('%08x: %s%*s %-*s %s' % ( print('%08x: %s%*s %-*s %s' % (
rattr.toff, rattr.toff,
treerepr(rid, tag) treerepr(tree, (rid, tag), t_depth, color)
if args.get('tree') or args.get('tree_rbyd') if args.get('tree') or args.get('tree_rbyd')
else '', else '',
2*w_width+1, '%d-%d' % (rid-(rattr.weight-1), rid) 2*w_width+1, '%d-%d' % (rid-(w-1), rid) if w > 1
if rattr.weight > 1 else rid if w > 0 or i == 0
else rid if rattr.weight > 0 or i == 0
else '', else '',
21+w_width, rattr.tagrepr(), 21+w_width, rattr.tagrepr(),
next(xxd(rattr[:8], 8), '') next(xxd(rattr[:8], 8), '')
@@ -1290,7 +1378,8 @@ def main(disk, blocks=None, *,
block_size = f.tell() block_size = f.tell()
# fetch the rbyd # fetch the rbyd
rbyd = Rbyd.fetch((f, block_size), blocks) bd = Bd(f, block_size, block_count)
rbyd = Rbyd.fetch(bd, blocks)
print('rbyd %s w%d, rev %08x, size %d, cksum %08x' % ( print('rbyd %s w%d, rev %08x, size %d, cksum %08x' % (
rbyd.addr(), rbyd.addr(),