#!/usr/bin/python3 import re import typing as T from dataclasses import dataclass from pathlib import Path GRID_MAX_SQUARES = 50 GRID_SQUARE_SIZE = 12 GRID_SQUARE_MARGIN = 2 LEGEND_SQUARE_SIZE = 12 LEGEND_SQUARE_MARGIN = 2 LEGEND_ROW_PADDING = 3 LEGEND_MARGIN = 15 TEXT_SIZE = 15 TEXT_MARGIN = 5 DOCS_DIR = Path(__file__).parent PROGRESS_TXT_FILE = DOCS_DIR / "progress.txt" PROGRESS_SVG_FILE = DOCS_DIR / "progress.svg" COLOR_DECOMPILED = "forestgreen" COLOR_NAMED = "lightpink" COLOR_TODO = "mistyrose" @dataclass class Function: name: str offset: int size: int flags: str @property def is_decompiled(self): return "+" in self.flags @property def is_named(self): return not self.name.startswith("sub_") def collect_functions() -> T.Iterable[Function]: for line in PROGRESS_TXT_FILE.open(): if line.startswith("#"): continue func_name, offset, size, flags = re.split(r"\s+", line.strip()) yield Function( name=func_name, offset=int(offset, 16), size=int(size, 16), flags=flags, ) @dataclass class BoundingBox: x1: int y1: int x2: int y2: int class Shape: @property def bounds(self) -> BoundingBox: raise NotImplementedError("not implemented") def render(self) -> str: raise NotImplementedError("not implemented") def get_common_bbox(shapes: T.List[Shape]) -> BoundingBox: return BoundingBox( x1=min(shape.bounds.x1 for shape in shapes), y1=min(shape.bounds.y1 for shape in shapes), x2=max(shape.bounds.x2 for shape in shapes), y2=max(shape.bounds.y2 for shape in shapes), ) @dataclass class Square(Shape): x: int y: int color: str size: int = GRID_SQUARE_SIZE @property def bounds(self) -> BoundingBox: return BoundingBox( x1=self.x, y1=self.y, x2=self.x + self.size, y2=self.y + self.size ) def render(self) -> str: return ( f"' ) @dataclass class Text(Shape): x: int y: int text: str @property def bounds(self) -> BoundingBox: return BoundingBox( x1=self.x, y1=self.y, x2=self.x, y2=self.y + TEXT_SIZE ) def render(self) -> str: return ( f'' f"{self.text}" f"" ) @dataclass class LegendText(Shape): x: int y: int color: str text: str @property def _square(self) -> Square: return Square( x=self.x, y=self.y + (TEXT_SIZE - LEGEND_SQUARE_SIZE) / 2, color=self.color, size=LEGEND_SQUARE_SIZE, ) @property def _text(self) -> Text: return Text( x=LEGEND_SQUARE_SIZE + TEXT_MARGIN, y=self.y, text=self.text, ) @property def bounds(self) -> BoundingBox: return get_common_bbox([self._square, self._text]) def render(self) -> str: return self._square.render() + self._text.render() def render_svg(all_functions: T.List[Function]) -> T.Iterable[Shape]: for i, function in enumerate(all_functions): x = (i % GRID_MAX_SQUARES) * (GRID_SQUARE_SIZE + GRID_SQUARE_MARGIN) y = (i // GRID_MAX_SQUARES) * (GRID_SQUARE_SIZE + GRID_SQUARE_MARGIN) if function.is_decompiled: color = COLOR_DECOMPILED elif function.is_named: color = COLOR_NAMED else: color = COLOR_TODO yield Square(x=x, y=y, color=color) y += GRID_SQUARE_SIZE + LEGEND_MARGIN ready_functions = [func for func in all_functions if func.is_decompiled] named_functions = [ func for func in all_functions if func.is_named and func not in ready_functions ] todo_functions = [ func for func in all_functions if func not in ready_functions and func not in named_functions ] def sum_size(functions: T.Iterable[Function]) -> int: return sum(func.size for func in functions) for (color, text, functions) in [ (COLOR_DECOMPILED, "Functions decompiled", ready_functions), ( COLOR_NAMED, "Functions not decompiled, but with known names", named_functions, ), ( COLOR_TODO, "Functions not decompiled, with unknown names", todo_functions, ), ]: yield LegendText( x=0, y=y, color=color, text=f"{text} (count): {len(functions)/len(all_functions):.02%}", ) y += TEXT_SIZE + LEGEND_ROW_PADDING yield LegendText( x=0, y=y, color=color, text=f"{text} (bytesize): {sum_size(functions)/sum_size(all_functions):.02%}", ) y += TEXT_SIZE + LEGEND_ROW_PADDING def main() -> None: functions = list(collect_functions()) with PROGRESS_SVG_FILE.open("w") as handle: shapes = list(render_svg(functions)) bbox = get_common_bbox(shapes) print( f'', file=handle, ) for shape in shapes: print(shape.render(), file=handle) print("", file=handle) if __name__ == "__main__": main()