aboutsummaryrefslogtreecommitdiffstats
path: root/experiments/modal_shot.py
blob: 2e6cd7a071588fbe6a7e2aef89006264ce3df479 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
"""Render a modal's chrome headless, with fake data, and print it.

    python3 experiments/modal_shot.py [name] [cols] [rows]
    python3 experiments/modal_shot.py --list

Chrome (borders, titles, spacing) is the one thing you cannot judge from the
pane you are working in, and opening a binary just to look at a dialog costs a
full auto-analysis. Every modal here is fed a stub, so this needs NO worker, NO
IDA and no .i64 -- it runs under any python with textual, in about a second.

The app it boots is a bare `App` carrying `IdaTui.CSS` and the app theme, which
is exactly what the modals resolve their styles against.
"""
import asyncio
import os
import sys

REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, REPO)

from textual.app import App, ComposeResult                    # noqa: E402
from textual.widgets import Static                            # noqa: E402

from idatui.app import (                                      # noqa: E402
    IDATUI_THEME, IdaTui, BusyScreen, ConfirmScreen, HelpScreen, LoadingScreen,
    QuitScreen, StructEditor, XrefsScreen,
)
from idatui.domain import Struct                              # noqa: E402
from idatui.rpc import screen_text                            # noqa: E402
from idatui._sync import wait_for                             # noqa: E402


class _StubProgram:
    """Just enough Program for the struct editor to render."""

    _STRUCTS = [
        Struct(name="timespec", size=0x10, members=2, is_union=False, ordinal=1),
        Struct(name="stat", size=0x90, members=15, is_union=False, ordinal=2),
        Struct(name="pthread_mutex_t", size=0x28, members=4, is_union=True,
               ordinal=3),
    ]
    _SRC = ("struct timespec\n"
            "{\n"
            "    __time_t tv_sec;   /* seconds */\n"
            "    __syscall_slong_t tv_nsec;\n"
            "};\n")

    def list_structs(self):
        return list(self._STRUCTS)

    def struct_source(self, name):
        return self._SRC.replace("timespec", name)


def _make(name: str):
    """name -> a mounted-ready modal screen."""
    if name == "structs":
        return StructEditor(_StubProgram())
    if name == "confirm":
        return ConfirmScreen("Delete struct 'timespec' ?",
                             "This cannot be undone.")
    if name == "quit":
        return QuitScreen(["echo", "libc.so.6"])
    if name == "help":
        return HelpScreen()
    if name == "busy":
        return BusyScreen("decompiling sub_2297\u2026")
    if name == "loading":
        return LoadingScreen("echo", "opening database\u2026")
    if name == "xrefs":
        return XrefsScreen(" xrefs to main", [
            (0x1234, "  sub_2297+0x1c    call    main"),
            (0x5678, "  _start+0x21      mov     rdi, main"),
        ])
    raise SystemExit(f"unknown modal {name!r}; --list to see them")


NAMES = ["structs", "confirm", "quit", "help", "busy", "loading", "xrefs"]


class _Shot(App):
    CSS = IdaTui.CSS

    def compose(self) -> ComposeResult:
        # A little content underneath, so the modal's edge is visible against
        # something rather than floating on an empty screen.
        yield Static("\n".join("  .... the view behind the dialog ...."
                               for _ in range(60)))

    def on_mount(self) -> None:
        self.register_theme(IDATUI_THEME)
        self.theme = IDATUI_THEME.name


async def main() -> None:
    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    if "--list" in sys.argv:
        print(" ".join(NAMES))
        return
    names = [args[0]] if args else NAMES
    cols = int(args[1]) if len(args) > 1 else 100
    rows = int(args[2]) if len(args) > 2 else 32

    for name in names:
        app = _Shot()
        async with app.run_test(size=(cols, rows)) as pilot:
            app.push_screen(_make(name))
            await pilot.pause()
            if name == "structs":
                await wait_for(
                    lambda: bool(getattr(app.screen, "_structs", None)),
                    pilot.pause, 5)
                app.screen.on_option_list_option_selected(
                    type("E", (), {"option_index": 0})())
                await wait_for(lambda: "{" in app.screen.query_one(
                    "#se-edit").text, pilot.pause, 5)
            await pilot.pause()
            print(f"\n=== {name} " + "=" * (cols - len(name) - 5))
            print(screen_text(app)["text"])


if __name__ == "__main__":
    asyncio.run(main())