mallocng: Various cleanups (#3130)

* dedup stride in meta; put group creation reason in aglib

* rename check4 to big_offset_check

* rename rnd-off to cyclic offset so it is more in line with the source

* more true-to-source cyclic checking

* Slot.preload() clarified; _slot added

* add Slot.preload_meta_dependants() so we actually have some sensible guarantees

* rework reserved logic to show the two different values

* add alt_value option to PropertyPrinter properties

* show the *0x10 values in brackets

* move Slot functions around a bit so they flow more logically

* remove erroneous assert
pull/3145/head
k4lizen 6 months ago committed by GitHub
parent 377197c0f1
commit e6dacb29df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -142,68 +142,105 @@ class Slot:
# The start of user memory. It may # The start of user memory. It may
# not be the actual start of the slot. # not be the actual start of the slot.
self.p: int = p self.p: int = p
# == The p header fields.
self._offset: int = None self._offset: int = None
# p[-3]. Stores lot's of different kinds of
# information.
self._pn3: int = None
self._idx: int = None self._idx: int = None
# Not exactly sure what this is. self._reserved_hd: int = None
self._check4: int = None self._big_offset_check: int = None
# ==
# == The footer fields.
self._reserved_ft: int = None
# ==
# == The start header fields.
self._start: int = None
self._cyclic_offset: int = None
# start[-3]. Stores whether we are cyclic.
self._startn3: int = None
# ==
self._reserved: int = None
self._group: Group = None self._group: Group = None
self._meta: Meta = None self._meta: Meta = None
self._reserved: int = None
def preload(self) -> None: def preload(self) -> None:
""" """
Read all the necessary process memory to populate the slot's Read all the necessary process memory to populate the slot's
fields. p header fields.
Do this if you know you will be using most of the Do this if you know you will be using most of the
fields of the slot. It will be faster, since we can do a few fields of the slot. It will be faster, since we can do a few
big reads instead of many small ones. You may also catch big reads instead of many small ones. You may also catch
inaccessible memory exceptions here and not worry about it later. inaccessible memory exceptions here and not worry about it later.
Fields dependant on the meta are not loaded - you will still
need to worry about exceptions coming from them.
Raises: Raises:
pwndbg.dbg_mod.Error: When reading memory fails. pwndbg.dbg_mod.Error: When reading memory fails.
""" """
# Read all the in-band data. # == Read the p header.
inband_data = memory.read(self.p - 8, 8) pheader = memory.read(self.p - 8, 8)
self._check4 = inband_data[4] self._big_offset_check = pheader[4]
if self._check4: if self._big_offset_check:
self._offset = int.from_bytes(inband_data[0:4], pwndbg.aglib.arch.endian, signed=False) self._offset = int.from_bytes(pheader[0:4], pwndbg.aglib.arch.endian, signed=False)
else:
self._offset = int.from_bytes(inband_data[6:8], pwndbg.aglib.arch.endian, signed=False)
idxv = inband_data[5]
if idxv != 255:
self._idx = idxv & 31
else: else:
self._idx = 0 self._offset = int.from_bytes(pheader[6:8], pwndbg.aglib.arch.endian, signed=False)
self._pn3 = pheader[5]
# ==
# Read the group's meta pointer. # Read the group's meta pointer.
_ = self.meta _ = self.meta
# Need this loaded for lots of fields,
# but we will let it be since we want to be able to
# say stuff about this slot even with a corrupt meta.
# _ = self.meta.stride
self._reserved = inband_data[5] >> 5 # To calculate footer and p header fields
if self._reserved == 5: # we need self.meta.stride. However we want to be able to
# self.end doesn't need a read. # return some information even if the meta is corrupt, so
self._reserved = memory.u32(self.end - 4) # we won't load that here.
# All the other fields are calculated without # Other fields are calculated without memory reads.
# memory reads.
@property def preload_meta_dependants(self) -> None:
def check4(self) -> int:
""" """
Preloads all fields that depend on a sane meta.
It generally only makes sense to run this after preload().
Calling this reduces the amount of process writes and centralizes
field exceptions to this function.
If both preload() and preload_meta_dependants() return without
exceptions, all the fields in this class are guaranteed to not
cause any more memory reads nor raise any more exceptions.
Raises: Raises:
pwndbg.dbg_mod.Error: When reading memory fails. pwndbg.dbg_mod.Error: When the meta is corrupt and/or
""" reading memory fails.
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L134 """
if self._check4 is None: # Make sure stride is valid.
self._check4 = memory.u8(self.p - 4) _ = self.meta.stride
# Read the start header only if we need to.
if self.start != self.p:
startheader = memory.read(self.start - 3, 3)
self._startn3 = int.from_bytes(startheader[0:1], pwndbg.aglib.arch.endian, signed=False)
self._cyclic_offset = int.from_bytes(
startheader[1:3], pwndbg.aglib.arch.endian, signed=False
)
# Read footer.
if self.reserved_in_header != 5:
self._reserved_ft = -1
else:
self._reserved_ft = memory.u32(self.end - 4)
return self._check4 # Other fields are calculated without memory reads.
# p header fields..
@property @property
def offset(self) -> int: def offset(self) -> int:
@ -213,15 +250,27 @@ class Slot:
""" """
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L132 # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L132
if self._offset is None: if self._offset is None:
if self.check4: if self.big_offset_check:
# assert(!offset); # This can only happen in aligned allocations, which is kind of
# weird. All allocations of this size are probably mmaped.
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/aligned_alloc.c#L49
self._offest = memory.u32(self.p - 8) self._offest = memory.u32(self.p - 8)
# assert(offset > 0xffff);
else: else:
self._offset = memory.u16(self.p - 2) self._offset = memory.u16(self.p - 2)
return self._offset return self._offset
@property
def pn3(self) -> int:
"""
Raises:
pwndbg.dbg_mod.Error: When reading memory fails.
"""
if self._pn3 is None:
self._pn3 = memory.u8(self.p - 3)
return self._pn3
@property @property
def idx(self) -> int: def idx(self) -> int:
""" """
@ -230,39 +279,108 @@ class Slot:
""" """
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L133 # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L133
if self._idx is None: if self._idx is None:
v = memory.u8(self.p - 3) if self.pn3 == 255:
if v != 255:
self._idx = v & 31
else:
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/donate.c#L29 # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/donate.c#L29
self._idx = 0 self._idx = 0
else:
self._idx = self.pn3 & 31
return self._idx return self._idx
@property @property
def group(self) -> Group: def reserved_in_header(self) -> int:
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L139 # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L193
if self._group is None: if self._reserved_hd is None:
self._group = Group(self.p - UNIT * self.offset - UNIT) self._reserved_hd = self.pn3 >> 5
return self._group return self._reserved_hd
@property @property
def meta(self) -> Meta: def big_offset_check(self) -> int:
""" """
Raises: Raises:
pwndbg.dbg_mod.Error: When reading memory fails. pwndbg.dbg_mod.Error: When reading memory fails.
""" """
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L140 # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L134
if self._meta is None: if self._big_offset_check is None:
self._meta = Meta(memory.read_pointer_width(self.group.addr)) self._big_offset_check = memory.u8(self.p - 4)
return self._meta return self._big_offset_check
# start header fields..
@property @property
def start(self) -> int: def start(self) -> int:
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/free.c#L108 """
return self.group.storage + self.meta.stride * self.idx Raises:
pwndbg.dbg_mod.Error: When reading meta fails.
"""
# We have this if-statement so Slot.from_start() can
# populate _start, giving us lots of fields even with
# a corrupt meta.
if self._start is None:
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/free.c#L108
self._start = self.group.storage + self.meta.stride * self.idx
return self._start
@property
def cyclic_offset(self) -> int:
"""
Returns zero if is_cyclic() is False.
Raises:
pwndbg.dbg_mod.Error: When reading meta fails.
"""
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L216
# Not sure why musl saves it, it doesn't seem to use it.
# We could calculate it more easily than musl does `(self.p - self.start) // UNIT`
# but let's report the actual in-band metadata in case the structure
# is partially corrupted.
if self._cyclic_offset is None:
if self.is_cyclic():
self._cyclic_offset = memory.u16(self.start - 2)
else:
self._cyclic_offset = 0
return self._cyclic_offset
@property
def startn3(self) -> int:
"""
Raises:
pwndbg.dbg_mod.Error: When reading memory fails.
"""
if self._startn3 is None:
if self.p == self.start:
# No need to read memory twice.
self._startn3 = self.pn3
else:
self._startn3 = memory.u8(self.start - 3)
return self._startn3
# footer fields..
@property
def reserved_in_footer(self) -> int:
"""
Returns -1 if the value is invalid, i.e.
reserved_in_header() != 5.
Raises:
pwndbg.dbg_mod.Error: When reading memory fails.
"""
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L161
if self._reserved_ft is None:
if self.reserved_in_header != 5:
self._reserved_ft = -1
else:
self._reserved_ft = memory.u32(self.end - 4)
return self._reserved_ft
# code variables..
@property @property
def end(self) -> int: def end(self) -> int:
@ -276,15 +394,28 @@ class Slot:
@property @property
def reserved(self) -> int: def reserved(self) -> int:
""" """
Returns 0 if reserved_in_header() == 6.
Returns -1 if reserved_in_header() == 7.
Raises: Raises:
pwndbg.dbg_mod.Error: When reading memory fails. pwndbg.dbg_mod.Error: When reading memory fails.
""" """
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L161 # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L161
# Lots of asserts here.. # Lots of asserts here..
if self._reserved is None: if self._reserved is None:
self._reserved = memory.u8(self.p - 3) >> 5 if self.reserved_in_header < 5:
if self._reserved == 5: self._reserved = self.reserved_in_header
self._reserved = memory.u32(self.end - 4) elif self.reserved_in_header == 5:
self._reserved = self.reserved_in_footer
elif self.reserved_in_header == 6:
# See contains_group()
self._reserved = 0
else:
# Value forced due to bit-size.
assert self.reserved_in_header == 7
# Should never happen. It is possible for start[-3]
# to contain (7<<5) but p[-3] can't.
return -1
return self._reserved return self._reserved
@ -314,23 +445,48 @@ class Slot:
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L199 # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L199
return (self.meta.stride - self.nominal_size - IB) // UNIT return (self.meta.stride - self.nominal_size - IB) // UNIT
# non-local..
@property
def group(self) -> Group:
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L139
if self._group is None:
self._group = Group(self.p - UNIT * self.offset - UNIT)
return self._group
@property @property
def internal_offset(self) -> int: def meta(self) -> Meta:
""" """
Raises: Raises:
pwndbg.dbg_mod.Error: When reading meta fails. pwndbg.dbg_mod.Error: When reading memory fails.
""" """
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L204 # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L140
# Not sure why musl saves it, it doesn't seem to use it. if self._meta is None:
# We can calculate it more easily than musl does: self._meta = Meta(memory.read_pointer_width(self.group.addr))
return (self.p - self.start) // UNIT
return self._meta
# checks..
def is_cyclic(self) -> int:
"""
Returns whether mallocng reports that p != start.
"""
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L217
# We could of course just do `return p != start`
# but we want to report the actual metadata in case the structure
# is partially corrupted.
return self.startn3 == 224
def contains_group(self) -> bool: def contains_group(self) -> bool:
""" """
Does this slot nest a group? Does this slot nest a group?
""" """
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/malloc.c#L269 # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/malloc.c#L269
return self.reserved == 6 return self.reserved_in_header == 6
# constructors..
@classmethod @classmethod
def from_p(cls, p: int) -> "Slot": def from_p(cls, p: int) -> "Slot":
@ -338,22 +494,25 @@ class Slot:
@classmethod @classmethod
def from_start(cls, start: int) -> "Slot": def from_start(cls, start: int) -> "Slot":
idx_or_marker = memory.u8(start - 3) # We need to check if we are cyclic or not.
if idx_or_marker == 224: # See is_cyclic() and cyclic_offset() logic.
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L217 sn3 = memory.u8(start - 3)
# p is at an offset from start if sn3 == 224:
# Read the cyclic offset to calculate it.
off = memory.u16(start - 2) off = memory.u16(start - 2)
p = start + off * UNIT p = start + off * UNIT
obj = cls(p) obj = cls(p)
obj._sn3 = sn3
else: else:
p = start p = start
obj = cls(p) obj = cls(p)
obj._sn3 = obj._pn3 = sn3
# FIXME: Not good if the slot is corrupted and we can't # FIXME: Not good if the slot is corrupted and we can't
# access the meta. # access the meta.
assert obj.start == start assert obj.start == start
obj._start = start
return obj return obj
@ -541,6 +700,8 @@ class Meta:
return self._maplen return self._maplen
# Semi-custom methods..
@property @property
def stride(self): def stride(self):
""" """
@ -559,6 +720,8 @@ class Meta:
return self._stride return self._stride
# Custom methods..
@property @property
def cnt(self): def cnt(self):
""" """
@ -568,17 +731,34 @@ class Meta:
return self.last_idx + 1 return self.last_idx + 1
@property @property
def slot_size(self): def is_donated(self) -> bool:
"""
Returns whether the group object referred to by this meta has been
created by being donated by ld.
""" """
The size of a slot in this group, in bytes. # When mapped object files contain unused memory, they are donated
# to the heap. See https://elixir.bootlin.com/musl/v1.2.5/source/ldso/dynlink.c#L600
# and https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/donate.c#L36 .
# Only in this case is `meta.freeable = 0;`
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/donate.c#L25
return not self.freeable
Returns -1 if sizeclass >= len(size_classes). @property
def is_mmaped(self) -> bool:
""" """
if self.sizeclass < len(size_classes): Returns whether the group object referred to by this meta has been
return size_classes[self.sizeclass] * UNIT created by being mmaped.
else: """
# The meta is corrupted. # https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L177
return -1 return not self.is_donated and not self.last_idx and bool(self.maplen)
@property
def is_nested(self) -> bool:
"""
Returns whether the group object referred to by this meta has been
created by being nested into a slot.
"""
return not self.is_donated and not self.is_mmaped
@staticmethod @staticmethod
def sizeof(): def sizeof():

@ -245,7 +245,7 @@ def dump_group(group: mallocng.Group) -> str:
pp = PropertyPrinter() pp = PropertyPrinter()
pp.start_section("group", group_range) pp.start_section("group", group_range)
pp.set_padding(2) pp.set_padding(5)
pp.add( pp.add(
[ [
Property(name="meta", value=group.meta.addr, is_addr=True), Property(name="meta", value=group.meta.addr, is_addr=True),
@ -256,7 +256,7 @@ def dump_group(group: mallocng.Group) -> str:
if group_size != -1: if group_size != -1:
pp.write("---\n") pp.write("---\n")
pp.set_padding(3) pp.set_padding(5)
pp.add( pp.add(
[ [
Property(name="group size", value=group_size), Property(name="group size", value=group_size),
@ -274,7 +274,7 @@ def dump_meta(meta: mallocng.Meta) -> str:
pp = PropertyPrinter() pp = PropertyPrinter()
pp.start_section("meta", "@ " + C.memory.get(meta.addr)) pp.start_section("meta", "@ " + C.memory.get(meta.addr))
pp.set_padding(2) pp.set_padding(5)
pp.add( pp.add(
[ [
Property(name="prev", value=meta.prev, is_addr=True), Property(name="prev", value=meta.prev, is_addr=True),
@ -289,23 +289,18 @@ def dump_meta(meta: mallocng.Meta) -> str:
] ]
) )
pp.write("---\n") pp.write("---\n")
pp.set_padding(3) pp.set_padding(9)
pp.add( pp.add(
[ [
Property(name="cnt", value=meta.cnt, extra="the number of slots"), Property(name="cnt", value=meta.cnt, extra="the number of slots"),
Property(name="slot size", value=meta.slot_size, extra='aka "stride"'), Property(name="stride", value=meta.stride),
] ]
) )
pp.end_section() pp.end_section()
output = pp.dump() output = pp.dump()
if not meta.freeable: if meta.is_donated:
# When mapped object files contain unused memory, they are donated
# to the heap. See https://elixir.bootlin.com/musl/v1.2.5/source/ldso/dynlink.c#L600
# and https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/donate.c#L36 .
# Only in this case is `meta.freeable = 0;`
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/donate.c#L25
output += C.bold("\nGroup donated by ld as unused part of ") output += C.bold("\nGroup donated by ld as unused part of ")
try: try:
@ -321,10 +316,10 @@ def dump_meta(meta: mallocng.Meta) -> str:
output += C.bold(".\n") output += C.bold(".\n")
elif not meta.last_idx and meta.maplen: elif meta.is_mmaped:
# https://elixir.bootlin.com/musl/v1.2.5/source/src/malloc/mallocng/meta.h#L177
output += C.bold("\nGroup allocated with mmap().\n") output += C.bold("\nGroup allocated with mmap().\n")
else: else:
assert meta.is_nested
output += C.bold("\nGroup nested in slot of another group") output += C.bold("\nGroup nested in slot of another group")
try: try:
parent_group = mallocng.Slot(mallocng.Group(meta.mem).addr).group.addr parent_group = mallocng.Slot(mallocng.Group(meta.mem).addr).group.addr
@ -383,8 +378,14 @@ def mallocng_slot_user(address: int, all: bool) -> None:
try: try:
slot.meta.preload() slot.meta.preload()
except pwndbg.dbg_mod.Error as e: try:
print(message.error(f"Error while reading meta: {e}")) slot.preload_meta_dependants()
except pwndbg.dbg_mod.Error as e1:
print(message.error(f"Error while loading slot fields that depend on the meta:\n{e1}"))
read_success = False
except pwndbg.dbg_mod.Error as e2:
print(message.error(f"Error while reading meta: {e2}"))
read_success = False read_success = False
if not read_success: if not read_success:
@ -395,7 +396,7 @@ def mallocng_slot_user(address: int, all: bool) -> None:
if not all: if not all:
pp.start_section("slab") pp.start_section("slab")
pp.set_padding(7) pp.set_padding(10)
if read_success: if read_success:
pp.add( pp.add(
[ [
@ -413,7 +414,7 @@ def mallocng_slot_user(address: int, all: bool) -> None:
if read_success: if read_success:
pp.start_section("general") pp.start_section("general")
pp.set_padding(2) pp.set_padding(5)
pp.add( pp.add(
[ [
Property(name="start", value=slot.start, is_addr=True), Property(name="start", value=slot.start, is_addr=True),
@ -423,36 +424,59 @@ def mallocng_slot_user(address: int, all: bool) -> None:
name="stride", value=slot.meta.stride, extra="distance between adjacent slots" name="stride", value=slot.meta.stride, extra="distance between adjacent slots"
), ),
Property(name="user size", value=slot.user_size, extra='aka "nominal size", `n`'), Property(name="user size", value=slot.user_size, extra='aka "nominal size", `n`'),
Property(name="slack", value=slot.slack, extra="slot's unused memory / 0x10"), Property(
name="slack",
value=slot.slack,
extra="slot's unused memory / 0x10",
alt_value=(slot.slack * mallocng.UNIT),
),
] ]
) )
pp.end_section() pp.end_section()
pp.start_section("in-band") pp.start_section("in-band")
pp.set_padding(4) pp.set_padding(2)
reserved_extra = ["end - p - n", ""] reserved_extra = ["describes: end - p - n"]
if slot.reserved >= 5: if slot.reserved_in_header == 5:
reserved_extra[1] = "located near slot end" reserved_extra.append("use ftr reserved")
if slot.reserved == 6: elif slot.reserved_in_header == 6:
reserved_extra.append("this slot is a nested group") reserved_extra.append("a nested group is in this slot")
else: elif slot.reserved_in_header == 7:
reserved_extra[1] = "located in slot header" reserved_extra.append("this should not be possible")
inband_group = [ inband_group = [
Property(name="offset", value=slot.offset, extra="distance to first slot / 0x10"), Property(
name="offset",
value=slot.offset,
extra="distance to first slot / 0x10",
alt_value=(slot.offset * mallocng.UNIT),
),
Property(name="index", value=slot.idx, extra="index of slot in its group"), Property(name="index", value=slot.idx, extra="index of slot in its group"),
Property(name="reserved", value=slot.reserved, extra=reserved_extra), Property(name="hdr reserved", value=slot.reserved_in_header, extra=reserved_extra),
] ]
if slot.reserved_in_header == 5:
ftrsv = "NA (meta error)"
if read_success:
ftrsv = slot.reserved_in_footer
inband_group.append(Property(name="ftr reserved", value=ftrsv))
if read_success: if read_success:
# While it is technically saved in-band, there is no way # Start header fields.
# for us to locate it without metadata. if slot.is_cyclic():
cyc_val = slot.cyclic_offset
cyc_val_alt = cyc_val * mallocng.UNIT
else:
cyc_val = "NA"
cyc_val_alt = "not cyclic"
inband_group.append( inband_group.append(
Property( Property(
name="rnd-off", name="cyclic offset",
value=slot.internal_offset, value=cyc_val,
extra="prevents double free, (p - start) / 0x10", extra="prevents double free, (p - start) / 0x10",
alt_value=cyc_val_alt,
), ),
) )

@ -20,6 +20,7 @@ class Property:
name: str name: str
value: Any value: Any
alt_value: Any = None
extra: str | List[str] = "" extra: str | List[str] = ""
is_addr: bool = False is_addr: bool = False
use_hex: bool = True use_hex: bool = True
@ -68,6 +69,11 @@ class PropertyPrinter:
prop.value = hex(prop.value) prop.value = hex(prop.value)
else: else:
prop.value = str(prop.value) prop.value = str(prop.value)
if isinstance(prop.alt_value, int):
if prop.use_hex:
prop.alt_value = hex(prop.alt_value)
else:
prop.alt_value = str(prop.alt_value)
# Get max lengths to calculate proper ljust # Get max lengths to calculate proper ljust
# + 1 to account for the ":" # + 1 to account for the ":"
@ -95,14 +101,17 @@ class PropertyPrinter:
else: else:
colored_val = self.value_color_func(prop.value) colored_val = self.value_color_func(prop.value)
self.text += color.ljust_colored(colored_val, max_value_len) colored_alt_val = ""
if prop.alt_value is not None:
colored_alt_val = " (" + self.value_color_func(prop.alt_value) + ")"
self.text += color.ljust_colored(colored_val + colored_alt_val, max_value_len)
if isinstance(prop.extra, str): if isinstance(prop.extra, str):
self.text += " " + prop.extra self.text += " " + prop.extra
else: else:
# list of strings, we want each one under the other # list of strings, we want each one under the other
assert isinstance(prop.extra, list) assert isinstance(prop.extra, list)
assert len(prop.extra) > 1
self.text += " " + prop.extra[0] self.text += " " + prop.extra[0]
for i in range(1, len(prop.extra)): for i in range(1, len(prop.extra)):

Loading…
Cancel
Save