dissect.cstruct 4.8.dev6__py3-none-any.whl → 4.8.dev8__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.
dissect/cstruct/utils.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
3
4
  import pprint
4
5
  import string
5
6
  import sys
@@ -13,15 +14,29 @@ if TYPE_CHECKING:
13
14
  from collections.abc import Iterator
14
15
  from typing import Literal
15
16
 
16
- COLOR_RED = "\033[1;31m"
17
- COLOR_GREEN = "\033[1;32m"
18
- COLOR_YELLOW = "\033[1;33m"
19
- COLOR_BLUE = "\033[1;34m"
20
- COLOR_PURPLE = "\033[1;35m"
21
- COLOR_CYAN = "\033[1;36m"
22
- COLOR_WHITE = "\033[1;37m"
23
- COLOR_NORMAL = "\033[1;0m"
24
-
17
+ # Regular ANSI colors
18
+ COLOR_RED = "\033[0;31m"
19
+ COLOR_GREEN = "\033[0;32m"
20
+ COLOR_YELLOW = "\033[0;93m"
21
+ COLOR_BLUE = "\033[0;34m"
22
+ COLOR_PURPLE = "\033[0;35m"
23
+ COLOR_CYAN = "\033[0;36m"
24
+ COLOR_WHITE = "\033[0;37m"
25
+ COLOR_BLACK = "\033[0;30m"
26
+ COLOR_GREY = "\033[0;90m"
27
+
28
+ # Bold ANSI colors
29
+ COLOR_RED_BOLD = "\033[1;31m"
30
+ COLOR_GREEN_BOLD = "\033[1;32m"
31
+ COLOR_YELLOW_BOLD = "\033[1;33m"
32
+ COLOR_BLUE_BOLD = "\033[1;34m"
33
+ COLOR_PURPLE_BOLD = "\033[1;35m"
34
+ COLOR_CYAN_BOLD = "\033[1;36m"
35
+ COLOR_WHITE_BOLD = "\033[1;37m"
36
+ COLOR_BLACK_BOLD = "\033[1;30m"
37
+ COLOR_GREY_BOLD = "\033[1;90m"
38
+
39
+ # Background ANSI colors
25
40
  COLOR_BG_RED = "\033[1;41m\033[1;37m"
26
41
  COLOR_BG_GREEN = "\033[1;42m\033[1;37m"
27
42
  COLOR_BG_YELLOW = "\033[1;43m\033[1;37m"
@@ -30,6 +45,10 @@ COLOR_BG_PURPLE = "\033[1;45m\033[1;37m"
30
45
  COLOR_BG_CYAN = "\033[1;46m\033[1;37m"
31
46
  COLOR_BG_WHITE = "\033[1;47m\033[1;30m"
32
47
 
48
+ # Reset ANSI codes
49
+ COLOR_CLEAR = "\033[0m"
50
+ COLOR_CLEAR_BOLD = "\033[1;0m"
51
+
33
52
  PRINTABLE = string.digits + string.ascii_letters + string.punctuation + " "
34
53
 
35
54
  ENDIANNESS_MAP: dict[str, Literal["big", "little"]] = {
@@ -44,20 +63,67 @@ ENDIANNESS_MAP: dict[str, Literal["big", "little"]] = {
44
63
  Palette = list[tuple[int, str]]
45
64
 
46
65
 
47
- def _hexdump(data: bytes, palette: Palette | None = None, offset: int = 0, prefix: str = "") -> Iterator[str]:
66
+ def _human_colors() -> dict[str, str]:
67
+ """Generates a dictionary of characters with a human-readable ANSI color they should be in a hexdump.
68
+
69
+ Coloring logic implementation derived from HexFriend and ImHex.
70
+ """
71
+ # Make all characters not in any rules below light green
72
+ colors = {chr(char): COLOR_GREEN for char in range(256)}
73
+
74
+ # Make all ASCII extended characters yellow
75
+ for char in colors:
76
+ if ord(char) & 0x80 == 0:
77
+ colors[char] = COLOR_YELLOW
78
+
79
+ # Make null bytes grey
80
+ colors["\00"] = COLOR_GREY
81
+
82
+ # Make printable ASCII characters bold white (0x32-0x7E)
83
+ for char in PRINTABLE:
84
+ colors[char] = COLOR_WHITE_BOLD
85
+
86
+ # Make ASCII whitespace characters green bold (0x9, 0xA, 0xB, 0xC, 0xD, 0x20)
87
+ for char in ("\t", "\n", "\11", "\12", "\r", "\20"):
88
+ colors[char] = COLOR_GREEN_BOLD
89
+
90
+ return colors
91
+
92
+
93
+ HUMAN_COLORS = _human_colors()
94
+
95
+
96
+ def _hexdump(
97
+ data: bytes,
98
+ *,
99
+ palette: Palette | None = None,
100
+ offset: int = 0,
101
+ prefix: str = "",
102
+ pretty: bool | None = False,
103
+ autoskip: bool = False,
104
+ ) -> Iterator[str]:
48
105
  """Hexdump some data.
49
106
 
50
107
  Args:
51
108
  data: Bytes to hexdump.
109
+ palette: Colorize the hexdump using this color pattern.
52
110
  offset: Byte offset of the hexdump.
53
111
  prefix: Optional prefix.
54
- palette: Colorize the hexdump using this color pattern.
112
+ pretty: Use pretty colors, mutual exclusive with palette.
113
+ autoskip: A single '*' replaces NUL-lines in the output.
55
114
  """
56
115
  if palette:
57
116
  palette = palette[::-1]
58
117
 
118
+ # only happy little accidents
119
+ if pretty and palette:
120
+ raise ValueError("Cannot use argument 'pretty' in combination with 'palette', please pick one")
121
+
59
122
  remaining = 0
60
123
  active = None
124
+ in_null_run = False
125
+ in_collapsed_null_run = False
126
+ last_offset = len(data) - 16
61
127
 
62
128
  for i in range(0, len(data), 16):
63
129
  values = ""
@@ -87,42 +153,80 @@ def _hexdump(data: bytes, palette: Palette | None = None, offset: int = 0, prefi
87
153
 
88
154
  if active:
89
155
  values += f"{ord(char):02x}"
90
- chars.append(active + print_char + COLOR_NORMAL)
156
+ chars.append(active + print_char + COLOR_CLEAR_BOLD)
91
157
  else:
92
- values += f"{ord(char):02x}"
93
- chars.append(print_char)
158
+ if pretty and (color := HUMAN_COLORS.get(char, "")):
159
+ values += f"{color}{ord(char):02x}{COLOR_CLEAR}"
160
+ chars.append(color + print_char + COLOR_CLEAR)
161
+ else:
162
+ values += f"{ord(char):02x}"
163
+ chars.append(print_char)
94
164
 
95
165
  remaining -= 1
96
166
  if remaining == 0:
97
167
  active = None
98
168
 
99
169
  if palette is not None:
100
- values += COLOR_NORMAL
170
+ values += COLOR_CLEAR_BOLD
101
171
 
102
172
  if j == 15 and palette is not None:
103
- values += COLOR_NORMAL
173
+ values += COLOR_CLEAR_BOLD
104
174
 
105
175
  values += " "
106
176
  if j == 7:
107
177
  values += " "
108
178
 
179
+ if autoskip and 0 < i < last_offset and data[i : i + 16] == b"\x00" * 16:
180
+ if in_null_run:
181
+ if not in_collapsed_null_run:
182
+ yield "*"
183
+ in_collapsed_null_run = True
184
+ continue
185
+
186
+ # Keep the first interior NUL line visible, collapse from the second onwards.
187
+ in_null_run = True
188
+ else:
189
+ in_null_run = False
190
+ in_collapsed_null_run = False
191
+
109
192
  chars = "".join(chars)
110
193
  yield f"{prefix}{offset + i:08x} {values:48s} {chars}"
111
194
 
112
195
 
113
196
  def hexdump(
114
- data: bytes, palette: Palette | None = None, offset: int = 0, prefix: str = "", output: str = "print"
197
+ data: bytes,
198
+ *,
199
+ palette: Palette | None = None,
200
+ offset: int = 0,
201
+ prefix: str = "",
202
+ output: str = "print",
203
+ pretty: bool | None = None,
204
+ autoskip: bool = False,
115
205
  ) -> Iterator[str] | str | None:
116
206
  """Hexdump some data.
117
207
 
208
+ Uses colored ANSI output with output type "print" by default. Disable with ``pretty=False``
209
+ or set the environment variable ``NO_COLOR``.
210
+
118
211
  Args:
119
212
  data: Bytes to hexdump.
120
213
  palette: Colorize the hexdump using this color pattern.
121
214
  offset: Byte offset of the hexdump.
122
215
  prefix: Optional prefix.
123
216
  output: Output format, can be 'print', 'generator' or 'string'.
217
+ pretty: Use pretty colors for improved human readability.
218
+ autoskip: A single '*' replaces NUL-lines in the output.
124
219
  """
125
- generator = _hexdump(data, palette, offset, prefix)
220
+ # Enable pretty colors by default if ...
221
+ if (
222
+ output == "print" # the output type is set to 'print'
223
+ and not palette # no palette is given (structdump only)
224
+ and pretty is not False # pretty was not explicitly set to False
225
+ and not os.environ.get("NO_COLOR") # and the environment allows colors
226
+ ):
227
+ pretty = True
228
+
229
+ generator = _hexdump(data, palette=palette, offset=offset, prefix=prefix, pretty=pretty, autoskip=autoskip)
126
230
  if output == "print":
127
231
  print("\n".join(generator))
128
232
  return None
@@ -139,16 +243,17 @@ def _dumpstruct(
139
243
  offset: int,
140
244
  color: bool,
141
245
  output: str,
246
+ autoskip: bool,
142
247
  ) -> str | None:
143
248
  palette = []
144
249
  colors = [
145
- (COLOR_RED, COLOR_BG_RED),
146
- (COLOR_GREEN, COLOR_BG_GREEN),
147
- (COLOR_YELLOW, COLOR_BG_YELLOW),
148
- (COLOR_BLUE, COLOR_BG_BLUE),
149
- (COLOR_PURPLE, COLOR_BG_PURPLE),
150
- (COLOR_CYAN, COLOR_BG_CYAN),
151
- (COLOR_WHITE, COLOR_BG_WHITE),
250
+ (COLOR_RED_BOLD, COLOR_BG_RED),
251
+ (COLOR_GREEN_BOLD, COLOR_BG_GREEN),
252
+ (COLOR_YELLOW_BOLD, COLOR_BG_YELLOW),
253
+ (COLOR_BLUE_BOLD, COLOR_BG_BLUE),
254
+ (COLOR_PURPLE_BOLD, COLOR_BG_PURPLE),
255
+ (COLOR_CYAN_BOLD, COLOR_BG_CYAN),
256
+ (COLOR_WHITE_BOLD, COLOR_BG_WHITE),
152
257
  ]
153
258
  ci = 0
154
259
  out = [f"struct {structure.__class__.__name__}:"]
@@ -172,7 +277,7 @@ def _dumpstruct(
172
277
  size = structure.__sizes__[field._name]
173
278
  palette.append((size, background))
174
279
  ci += 1
175
- out.append(f"- {foreground}{field._name}{COLOR_NORMAL}: {value}")
280
+ out.append(f"- {foreground}{field._name}{COLOR_CLEAR_BOLD}: {value}")
176
281
  else:
177
282
  out.append(f"- {field._name}: {value}")
178
283
 
@@ -180,11 +285,11 @@ def _dumpstruct(
180
285
 
181
286
  if output == "print":
182
287
  print()
183
- hexdump(data, palette, offset=offset)
288
+ hexdump(data, palette=palette, offset=offset, autoskip=autoskip)
184
289
  print()
185
290
  print(out)
186
291
  elif output == "string":
187
- return f"\n{hexdump(data, palette, offset=offset, output='string')}\n\n{out}"
292
+ return f"\n{hexdump(data, palette=palette, offset=offset, output='string', autoskip=autoskip)}\n\n{out}"
188
293
  return None
189
294
 
190
295
 
@@ -193,6 +298,7 @@ def dumpstruct(
193
298
  data: bytes | None = None,
194
299
  offset: int = 0,
195
300
  color: bool = True,
301
+ autoskip: bool = False,
196
302
  output: str = "print",
197
303
  ) -> str | None:
198
304
  """Dump a structure or parsed structure instance.
@@ -203,15 +309,17 @@ def dumpstruct(
203
309
  obj: Structure to dump.
204
310
  data: Bytes to parse the Structure on, if obj is not a parsed Structure already.
205
311
  offset: Byte offset of the hexdump.
312
+ color: Colorize the hexdump and structure output.
313
+ autoskip: A single '*' replaces NUL-lines in the output.
206
314
  output: Output format, can be 'print' or 'string'.
207
315
  """
208
316
  if output not in ("print", "string"):
209
317
  raise ValueError(f"Invalid output argument: {output!r} (should be 'print' or 'string').")
210
318
 
211
319
  if isinstance(obj, Structure):
212
- return _dumpstruct(obj, obj.dumps(), offset, color, output)
320
+ return _dumpstruct(obj, obj.dumps(), offset, color, output, autoskip)
213
321
  if issubclass(obj, Structure) and data is not None:
214
- return _dumpstruct(obj(data), data, offset, color, output)
322
+ return _dumpstruct(obj(data), data, offset, color, output, autoskip)
215
323
  raise ValueError("Invalid arguments")
216
324
 
217
325
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dissect.cstruct
3
- Version: 4.8.dev6
3
+ Version: 4.8.dev8
4
4
  Summary: A Dissect module implementing a parser for C-like structures: structure parsing in Python made easy
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License-Expression: Apache-2.0
@@ -5,7 +5,7 @@ dissect/cstruct/cstruct.py,sha256=1lHbxZwj5x2EmmXLR0sJlp-wQqaTtd_ht1lvwit0KtQ,19
5
5
  dissect/cstruct/exceptions.py,sha256=SX05-_HqQn8lYC7SBHv5OrYEepYRMNmRiIzVPW8bvMQ,330
6
6
  dissect/cstruct/expression.py,sha256=mf5GFGLAn_2Tz3QK86Yqoi317l8cjMcffBKzP2STE6M,10841
7
7
  dissect/cstruct/parser.py,sha256=Mk8QbkDov_xfauZnp2MqUmFGOPZlp7O2rSLYbB9S3g4,24550
8
- dissect/cstruct/utils.py,sha256=6cU1QHTZnHXQaPoxMH93ELkwIIUgYw-TtxieEcVvYvw,10537
8
+ dissect/cstruct/utils.py,sha256=jSWZY8AWoLHF1W9ix-7EH2eI7pTHzLfnSfQHWH1qgKM,14190
9
9
  dissect/cstruct/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  dissect/cstruct/tools/stubgen.py,sha256=wvl07h3VnJZWe9ygX4QMxjEH1ZcWaI_6_pSdAJoOEXk,7820
11
11
  dissect/cstruct/types/__init__.py,sha256=6P71Pu94Dv7RNjZpDZ19pqzx-G0QzmQw4q0-3zWALso,911
@@ -20,10 +20,10 @@ dissect/cstruct/types/pointer.py,sha256=PqeJOoNBUfMd5uO2kpF6OHeW7Q6R1N1W1V4q1Mak
20
20
  dissect/cstruct/types/structure.py,sha256=I9LVeZ81rNE5GDT6wxpUL-PqMO31Ae6TVLLzTGyd6mM,33012
21
21
  dissect/cstruct/types/void.py,sha256=Gvh0HqI6jQSj2uesdo3J8-G41brWCz8pmP16l8xTBxE,1074
22
22
  dissect/cstruct/types/wchar.py,sha256=N9Y_XX2_hZEe2DwepJjxsB6xuRJ6zINRalcUofR59kY,2608
23
- dissect_cstruct-4.8.dev6.dist-info/licenses/COPYRIGHT,sha256=H-18RXfshdH9AdHwW2eO1Xa-5s6tY5eipHh5c0whDu4,316
24
- dissect_cstruct-4.8.dev6.dist-info/licenses/LICENSE,sha256=PhUqiw6jAh2KbBdVRPBq_hfAvfcTBin7nZ3CK7NQbTM,11341
25
- dissect_cstruct-4.8.dev6.dist-info/METADATA,sha256=pY7CiLwA8RVVBdf6iR3YhgUST9cRXtAT2-nGhWQ2tAQ,9255
26
- dissect_cstruct-4.8.dev6.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
27
- dissect_cstruct-4.8.dev6.dist-info/entry_points.txt,sha256=z53zqZqwD2OLrAkRwrP4wTeiU9CQe7xrMly0T2c0_wQ,71
28
- dissect_cstruct-4.8.dev6.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
29
- dissect_cstruct-4.8.dev6.dist-info/RECORD,,
23
+ dissect_cstruct-4.8.dev8.dist-info/licenses/COPYRIGHT,sha256=H-18RXfshdH9AdHwW2eO1Xa-5s6tY5eipHh5c0whDu4,316
24
+ dissect_cstruct-4.8.dev8.dist-info/licenses/LICENSE,sha256=PhUqiw6jAh2KbBdVRPBq_hfAvfcTBin7nZ3CK7NQbTM,11341
25
+ dissect_cstruct-4.8.dev8.dist-info/METADATA,sha256=LpS2_xD11bTw0F2YOz0X9ZWNDgptmNd_FcgzHnrR8nI,9255
26
+ dissect_cstruct-4.8.dev8.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
27
+ dissect_cstruct-4.8.dev8.dist-info/entry_points.txt,sha256=z53zqZqwD2OLrAkRwrP4wTeiU9CQe7xrMly0T2c0_wQ,71
28
+ dissect_cstruct-4.8.dev8.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
29
+ dissect_cstruct-4.8.dev8.dist-info/RECORD,,