dbxdebug 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dbxdebug/__init__.py +134 -0
- dbxdebug/capture_io.py +210 -0
- dbxdebug/cli.py +618 -0
- dbxdebug/dbx_kbd.py +392 -0
- dbxdebug/gdb.py +297 -0
- dbxdebug/html.py +559 -0
- dbxdebug/keyboard.py +205 -0
- dbxdebug/qmp.py +239 -0
- dbxdebug/utils.py +92 -0
- dbxdebug/video.py +239 -0
- dbxdebug-0.2.1.dist-info/METADATA +164 -0
- dbxdebug-0.2.1.dist-info/RECORD +14 -0
- dbxdebug-0.2.1.dist-info/WHEEL +4 -0
- dbxdebug-0.2.1.dist-info/entry_points.txt +2 -0
dbxdebug/keyboard.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Keyboard input helpers for QMP protocol.
|
|
3
|
+
|
|
4
|
+
Provides convenient functions for building key sequences and common
|
|
5
|
+
keyboard operations using the QMP protocol.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .dbx_kbd import DBX_KEY, DBX_KEY_TO_QCODE
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_qcode(key: DBX_KEY) -> str:
|
|
12
|
+
"""
|
|
13
|
+
Get QMP qcode for a DBX_KEY.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
key: DOSBox-X key code
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
QMP qcode string
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
ValueError: If key has no QMP mapping
|
|
23
|
+
"""
|
|
24
|
+
qcode = DBX_KEY_TO_QCODE.get(key)
|
|
25
|
+
if qcode is None:
|
|
26
|
+
raise ValueError(f"No QMP mapping for key: {key.name}")
|
|
27
|
+
return qcode
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def key_list(*keys: DBX_KEY) -> list[str]:
|
|
31
|
+
"""
|
|
32
|
+
Convert DBX_KEY values to QMP qcode list.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
*keys: DBX_KEY values
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of QMP qcode strings for use with QMPClient.send_key()
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
>>> key_list(DBX_KEY.KBD_leftctrl, DBX_KEY.KBD_c)
|
|
42
|
+
['ctrl', 'c']
|
|
43
|
+
"""
|
|
44
|
+
return [get_qcode(k) for k in keys]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def ctrl_key(key: DBX_KEY) -> list[str]:
|
|
48
|
+
"""
|
|
49
|
+
Build Ctrl+key combination.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
key: The key to combine with Ctrl
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
List of qcodes for QMPClient.send_key()
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
>>> ctrl_key(DBX_KEY.KBD_c)
|
|
59
|
+
['ctrl', 'c']
|
|
60
|
+
"""
|
|
61
|
+
return ["ctrl", get_qcode(key)]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def alt_key(key: DBX_KEY) -> list[str]:
|
|
65
|
+
"""
|
|
66
|
+
Build Alt+key combination.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
key: The key to combine with Alt
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
List of qcodes for QMPClient.send_key()
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
>>> alt_key(DBX_KEY.KBD_f4)
|
|
76
|
+
['alt', 'f4']
|
|
77
|
+
"""
|
|
78
|
+
return ["alt", get_qcode(key)]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def shift_key(key: DBX_KEY) -> list[str]:
|
|
82
|
+
"""
|
|
83
|
+
Build Shift+key combination.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
key: The key to combine with Shift
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
List of qcodes for QMPClient.send_key()
|
|
90
|
+
"""
|
|
91
|
+
return ["shift", get_qcode(key)]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def ctrl_alt_key(key: DBX_KEY) -> list[str]:
|
|
95
|
+
"""
|
|
96
|
+
Build Ctrl+Alt+key combination.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
key: The key to combine with Ctrl+Alt
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
List of qcodes for QMPClient.send_key()
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
>>> ctrl_alt_key(DBX_KEY.KBD_delete)
|
|
106
|
+
['ctrl', 'alt', 'delete']
|
|
107
|
+
"""
|
|
108
|
+
return ["ctrl", "alt", get_qcode(key)]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def ctrl_shift_key(key: DBX_KEY) -> list[str]:
|
|
112
|
+
"""
|
|
113
|
+
Build Ctrl+Shift+key combination.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
key: The key to combine with Ctrl+Shift
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
List of qcodes for QMPClient.send_key()
|
|
120
|
+
"""
|
|
121
|
+
return ["ctrl", "shift", get_qcode(key)]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# Convenience constants for common keys
|
|
125
|
+
ENTER = ["ret"]
|
|
126
|
+
ESCAPE = ["esc"]
|
|
127
|
+
TAB = ["tab"]
|
|
128
|
+
BACKSPACE = ["backspace"]
|
|
129
|
+
SPACE = ["spc"]
|
|
130
|
+
DELETE = ["delete"]
|
|
131
|
+
INSERT = ["insert"]
|
|
132
|
+
HOME = ["home"]
|
|
133
|
+
END = ["end"]
|
|
134
|
+
PAGE_UP = ["pgup"]
|
|
135
|
+
PAGE_DOWN = ["pgdn"]
|
|
136
|
+
UP = ["up"]
|
|
137
|
+
DOWN = ["down"]
|
|
138
|
+
LEFT = ["left"]
|
|
139
|
+
RIGHT = ["right"]
|
|
140
|
+
|
|
141
|
+
# Common key combinations
|
|
142
|
+
CTRL_C = ["ctrl", "c"]
|
|
143
|
+
CTRL_V = ["ctrl", "v"]
|
|
144
|
+
CTRL_X = ["ctrl", "x"]
|
|
145
|
+
CTRL_Z = ["ctrl", "z"]
|
|
146
|
+
CTRL_A = ["ctrl", "a"]
|
|
147
|
+
CTRL_S = ["ctrl", "s"]
|
|
148
|
+
CTRL_ALT_DEL = ["ctrl", "alt", "delete"]
|
|
149
|
+
ALT_F4 = ["alt", "f4"]
|
|
150
|
+
ALT_TAB = ["alt", "tab"]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def function_key(n: int) -> list[str]:
|
|
154
|
+
"""
|
|
155
|
+
Get function key qcode.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
n: Function key number (1-24)
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
List with single qcode for QMPClient.send_key()
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
>>> function_key(1)
|
|
165
|
+
['f1']
|
|
166
|
+
"""
|
|
167
|
+
if not 1 <= n <= 24:
|
|
168
|
+
raise ValueError(f"Function key must be 1-24, got {n}")
|
|
169
|
+
return [f"f{n}"]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def digit_key(n: int) -> list[str]:
|
|
173
|
+
"""
|
|
174
|
+
Get digit key qcode.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
n: Digit (0-9)
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List with single qcode for QMPClient.send_key()
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
>>> digit_key(5)
|
|
184
|
+
['5']
|
|
185
|
+
"""
|
|
186
|
+
if not 0 <= n <= 9:
|
|
187
|
+
raise ValueError(f"Digit must be 0-9, got {n}")
|
|
188
|
+
return [str(n)]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def number_keys(num: int) -> list[list[str]]:
|
|
192
|
+
"""
|
|
193
|
+
Get key sequences to type a number.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
num: Number to type
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of key lists, each to be passed to QMPClient.send_key()
|
|
200
|
+
|
|
201
|
+
Example:
|
|
202
|
+
>>> number_keys(42)
|
|
203
|
+
[['4'], ['2']]
|
|
204
|
+
"""
|
|
205
|
+
return [[d] for d in str(num)]
|
dbxdebug/qmp.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QEMU Monitor Protocol (QMP) client for DOSBox-X keyboard input.
|
|
3
|
+
|
|
4
|
+
Provides keyboard input injection via the QMP protocol:
|
|
5
|
+
- send_key(): Press and release keys with timing control
|
|
6
|
+
- key_down()/key_up(): Explicit press/release control
|
|
7
|
+
- type_text(): Type a string of characters
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import socket
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
from loguru import logger
|
|
15
|
+
|
|
16
|
+
from .dbx_kbd import DBX_KEY, DBX_KEY_TO_QCODE, char_needs_shift, char_to_qcode
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class QMPError(Exception):
|
|
20
|
+
"""QMP protocol error."""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class QMPClient:
|
|
26
|
+
"""QEMU Monitor Protocol client for DOSBox-X keyboard input."""
|
|
27
|
+
|
|
28
|
+
DEFAULT_PORT = 4444
|
|
29
|
+
|
|
30
|
+
def __init__(self, host: str = "localhost", port: int = DEFAULT_PORT):
|
|
31
|
+
"""
|
|
32
|
+
Connect to DOSBox-X QMP server.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
host: Server hostname
|
|
36
|
+
port: Server port (default 4444)
|
|
37
|
+
"""
|
|
38
|
+
logger.debug(f"Connecting to QMP server at {host}:{port}")
|
|
39
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
40
|
+
self.sock.connect((host, port))
|
|
41
|
+
self.buffer = ""
|
|
42
|
+
|
|
43
|
+
# Read greeting
|
|
44
|
+
greeting = self._read_message()
|
|
45
|
+
logger.debug(f"QMP greeting: {greeting}")
|
|
46
|
+
|
|
47
|
+
if "QMP" not in greeting:
|
|
48
|
+
raise QMPError(f"Unexpected QMP greeting: {greeting}")
|
|
49
|
+
|
|
50
|
+
# Send capabilities negotiation
|
|
51
|
+
self._send_command("qmp_capabilities")
|
|
52
|
+
logger.debug("QMP capabilities negotiated")
|
|
53
|
+
|
|
54
|
+
def _send_raw(self, data: str) -> None:
|
|
55
|
+
"""Send raw string to server."""
|
|
56
|
+
if self.sock is None:
|
|
57
|
+
raise ConnectionError("Socket not initialized")
|
|
58
|
+
self.sock.sendall((data + "\r\n").encode())
|
|
59
|
+
|
|
60
|
+
def _read_message(self) -> dict:
|
|
61
|
+
"""Read a JSON message from the server."""
|
|
62
|
+
if self.sock is None:
|
|
63
|
+
raise ConnectionError("Socket not initialized")
|
|
64
|
+
|
|
65
|
+
while True:
|
|
66
|
+
# Look for complete JSON object in buffer
|
|
67
|
+
if self.buffer:
|
|
68
|
+
try:
|
|
69
|
+
# Try to parse what we have
|
|
70
|
+
msg = json.loads(self.buffer.strip())
|
|
71
|
+
self.buffer = ""
|
|
72
|
+
return msg
|
|
73
|
+
except json.JSONDecodeError:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
# Need more data
|
|
77
|
+
data = self.sock.recv(4096).decode()
|
|
78
|
+
if not data:
|
|
79
|
+
raise ConnectionError("Connection closed")
|
|
80
|
+
self.buffer += data
|
|
81
|
+
|
|
82
|
+
# Try to find complete message (newline-delimited)
|
|
83
|
+
if "\n" in self.buffer:
|
|
84
|
+
line, self.buffer = self.buffer.split("\n", 1)
|
|
85
|
+
if line.strip():
|
|
86
|
+
return json.loads(line.strip())
|
|
87
|
+
|
|
88
|
+
def _send_command(self, execute: str, arguments: dict | None = None) -> dict:
|
|
89
|
+
"""
|
|
90
|
+
Send a QMP command and return the response.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
execute: Command name
|
|
94
|
+
arguments: Optional command arguments
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Response dict
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
QMPError: If command returns an error
|
|
101
|
+
"""
|
|
102
|
+
cmd: dict = {"execute": execute}
|
|
103
|
+
if arguments:
|
|
104
|
+
cmd["arguments"] = arguments
|
|
105
|
+
|
|
106
|
+
self._send_raw(json.dumps(cmd))
|
|
107
|
+
response = self._read_message()
|
|
108
|
+
|
|
109
|
+
if "error" in response:
|
|
110
|
+
error = response["error"]
|
|
111
|
+
raise QMPError(f"{error.get('class', 'Error')}: {error.get('desc', 'Unknown error')}")
|
|
112
|
+
|
|
113
|
+
return response
|
|
114
|
+
|
|
115
|
+
def send_key(self, keys: list[str], hold_time: int = 100) -> None:
|
|
116
|
+
"""
|
|
117
|
+
Send simultaneous key presses with auto-release.
|
|
118
|
+
|
|
119
|
+
All keys are pressed, held for hold_time ms, then released in reverse order.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
keys: List of QMP qcode strings (e.g., ["ctrl", "alt", "delete"])
|
|
123
|
+
hold_time: Milliseconds to hold before releasing (default 100)
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
>>> qmp.send_key(["ctrl", "c"]) # Ctrl+C
|
|
127
|
+
>>> qmp.send_key(["a"]) # Press 'a'
|
|
128
|
+
"""
|
|
129
|
+
key_objects = [{"type": "qcode", "data": k} for k in keys]
|
|
130
|
+
self._send_command("send-key", {"keys": key_objects, "hold-time": hold_time})
|
|
131
|
+
|
|
132
|
+
def send_key_dbx(self, keys: list[DBX_KEY], hold_time: int = 100) -> None:
|
|
133
|
+
"""
|
|
134
|
+
Send keys using DBX_KEY enum values.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
keys: List of DBX_KEY values
|
|
138
|
+
hold_time: Milliseconds to hold before releasing
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
>>> qmp.send_key_dbx([DBX_KEY.KBD_leftctrl, DBX_KEY.KBD_c])
|
|
142
|
+
"""
|
|
143
|
+
qcodes = []
|
|
144
|
+
for key in keys:
|
|
145
|
+
qcode = DBX_KEY_TO_QCODE.get(key)
|
|
146
|
+
if qcode is None:
|
|
147
|
+
raise ValueError(f"No QMP mapping for key: {key.name}")
|
|
148
|
+
qcodes.append(qcode)
|
|
149
|
+
self.send_key(qcodes, hold_time)
|
|
150
|
+
|
|
151
|
+
def key_down(self, key: str) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Send a key press (down) event.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
key: QMP qcode string
|
|
157
|
+
"""
|
|
158
|
+
event = {
|
|
159
|
+
"type": "key",
|
|
160
|
+
"data": {"down": True, "key": {"type": "qcode", "data": key}},
|
|
161
|
+
}
|
|
162
|
+
self._send_command("input-send-event", {"events": [event]})
|
|
163
|
+
|
|
164
|
+
def key_up(self, key: str) -> None:
|
|
165
|
+
"""
|
|
166
|
+
Send a key release (up) event.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
key: QMP qcode string
|
|
170
|
+
"""
|
|
171
|
+
event = {
|
|
172
|
+
"type": "key",
|
|
173
|
+
"data": {"down": False, "key": {"type": "qcode", "data": key}},
|
|
174
|
+
}
|
|
175
|
+
self._send_command("input-send-event", {"events": [event]})
|
|
176
|
+
|
|
177
|
+
def key_press(self, key: str, hold_time: float = 0.05) -> None:
|
|
178
|
+
"""
|
|
179
|
+
Press and release a single key with timing control.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
key: QMP qcode string
|
|
183
|
+
hold_time: Seconds to hold (default 0.05)
|
|
184
|
+
"""
|
|
185
|
+
self.key_down(key)
|
|
186
|
+
time.sleep(hold_time)
|
|
187
|
+
self.key_up(key)
|
|
188
|
+
|
|
189
|
+
def type_text(self, text: str, delay: float = 0.05) -> None:
|
|
190
|
+
"""
|
|
191
|
+
Type a string of text.
|
|
192
|
+
|
|
193
|
+
Handles shift for uppercase and special characters.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
text: Text to type
|
|
197
|
+
delay: Delay between characters in seconds (default 0.05)
|
|
198
|
+
|
|
199
|
+
Example:
|
|
200
|
+
>>> qmp.type_text("Hello World!")
|
|
201
|
+
"""
|
|
202
|
+
for char in text:
|
|
203
|
+
qcode = char_to_qcode(char)
|
|
204
|
+
if qcode is None:
|
|
205
|
+
logger.warning(f"Cannot type character: {repr(char)}")
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
if char_needs_shift(char):
|
|
209
|
+
# Hold shift, press key, release key, release shift
|
|
210
|
+
self.key_down("shift")
|
|
211
|
+
time.sleep(0.01)
|
|
212
|
+
self.key_press(qcode, 0.03)
|
|
213
|
+
self.key_up("shift")
|
|
214
|
+
else:
|
|
215
|
+
self.key_press(qcode, 0.03)
|
|
216
|
+
|
|
217
|
+
time.sleep(delay)
|
|
218
|
+
|
|
219
|
+
def query_commands(self) -> list[str]:
|
|
220
|
+
"""
|
|
221
|
+
Query available QMP commands.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
List of command names
|
|
225
|
+
"""
|
|
226
|
+
response = self._send_command("query-commands")
|
|
227
|
+
return [cmd["name"] for cmd in response.get("return", [])]
|
|
228
|
+
|
|
229
|
+
def close(self) -> None:
|
|
230
|
+
"""Close the connection."""
|
|
231
|
+
if self.sock:
|
|
232
|
+
self.sock.close()
|
|
233
|
+
self.sock = None # type: ignore
|
|
234
|
+
|
|
235
|
+
def __enter__(self) -> "QMPClient":
|
|
236
|
+
return self
|
|
237
|
+
|
|
238
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
239
|
+
self.close()
|
dbxdebug/utils.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for address parsing and hex dumps.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_x86_address(address_str: str | int) -> int:
|
|
9
|
+
"""
|
|
10
|
+
Parse an x86 address and convert to linear address.
|
|
11
|
+
|
|
12
|
+
Supports:
|
|
13
|
+
- Segmented addresses: "XXXX:YYYY" or "0xXXXX:YYYY"
|
|
14
|
+
- Linear hex: "0x12345" or "12345"
|
|
15
|
+
- Integer passthrough
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
address_str: Address as string or int
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Linear address as integer
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
ValueError: If address format is invalid
|
|
25
|
+
"""
|
|
26
|
+
if isinstance(address_str, int):
|
|
27
|
+
return address_str
|
|
28
|
+
|
|
29
|
+
# Check if it's a segmented address
|
|
30
|
+
segment_match = re.match(r"(?:0x)?([0-9a-fA-F]+):([0-9a-fA-F]+)", address_str)
|
|
31
|
+
if segment_match:
|
|
32
|
+
segment = int(segment_match.group(1), 16)
|
|
33
|
+
offset = int(segment_match.group(2), 16)
|
|
34
|
+
# Linear address: segment * 16 + offset
|
|
35
|
+
return (segment << 4) + offset
|
|
36
|
+
|
|
37
|
+
# Try to interpret as a normal hex or decimal number
|
|
38
|
+
try:
|
|
39
|
+
return int(address_str, 0)
|
|
40
|
+
except ValueError as e:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"Invalid address format: {address_str}. "
|
|
43
|
+
"Use segment:offset (e.g., b800:0000) or linear address."
|
|
44
|
+
) from e
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def hexdump(
|
|
48
|
+
src: bytes,
|
|
49
|
+
bytes_per_line: int = 32,
|
|
50
|
+
bytes_per_group: int = 4,
|
|
51
|
+
sep: str = ".",
|
|
52
|
+
start_addr: int = 0,
|
|
53
|
+
) -> list[str]:
|
|
54
|
+
"""
|
|
55
|
+
Generate a hex dump of binary data.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
src: Binary data to dump
|
|
59
|
+
bytes_per_line: Number of bytes per line
|
|
60
|
+
bytes_per_group: Group bytes with spaces
|
|
61
|
+
sep: Character for non-printable bytes
|
|
62
|
+
start_addr: Starting address for display
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
List of formatted hex dump lines
|
|
66
|
+
"""
|
|
67
|
+
FILTER = "".join([(len(repr(chr(x))) == 3) and chr(x) or sep for x in range(256)])
|
|
68
|
+
lines = []
|
|
69
|
+
max_addr_len = max(8, len(hex(start_addr + len(src))))
|
|
70
|
+
|
|
71
|
+
for offset in range(0, len(src), bytes_per_line):
|
|
72
|
+
addr = start_addr + offset
|
|
73
|
+
chars = src[offset : offset + bytes_per_line]
|
|
74
|
+
|
|
75
|
+
# Create hex string
|
|
76
|
+
hex_parts = []
|
|
77
|
+
for i in range(0, len(chars), bytes_per_group):
|
|
78
|
+
group = chars[i : i + bytes_per_group]
|
|
79
|
+
hex_parts.append("".join(f"{b:02X}" for b in group))
|
|
80
|
+
hex_string = " ".join(hex_parts)
|
|
81
|
+
|
|
82
|
+
# Pad to align with full lines
|
|
83
|
+
full_groups = bytes_per_line // bytes_per_group
|
|
84
|
+
expected_len = full_groups * (bytes_per_group * 2) + (full_groups - 1)
|
|
85
|
+
hex_string = hex_string.ljust(expected_len)
|
|
86
|
+
|
|
87
|
+
# Create printable string
|
|
88
|
+
printable = "".join((x <= 127 and FILTER[x]) or sep for x in chars)
|
|
89
|
+
|
|
90
|
+
lines.append(f"{addr:0{max_addr_len}X} {hex_string} |{printable}|")
|
|
91
|
+
|
|
92
|
+
return lines
|