dissect.cstruct 4.8.dev6__tar.gz → 4.8.dev8__tar.gz

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.
Files changed (77) hide show
  1. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/PKG-INFO +1 -1
  2. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/utils.py +138 -30
  3. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect.cstruct.egg-info/PKG-INFO +1 -1
  4. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_utils.py +132 -1
  5. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/.git-blame-ignore-revs +0 -0
  6. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/.gitattributes +0 -0
  7. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/CHANGELOG.md +0 -0
  8. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/COPYRIGHT +0 -0
  9. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/LICENSE +0 -0
  10. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/MANIFEST.in +0 -0
  11. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/README.md +0 -0
  12. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/__init__.py +0 -0
  13. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/bitbuffer.py +0 -0
  14. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/compiler.py +0 -0
  15. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/cstruct.py +0 -0
  16. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/exceptions.py +0 -0
  17. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/expression.py +0 -0
  18. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/parser.py +0 -0
  19. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/tools/__init__.py +0 -0
  20. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/tools/stubgen.py +0 -0
  21. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/types/__init__.py +0 -0
  22. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/types/base.py +0 -0
  23. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/types/char.py +0 -0
  24. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/types/enum.py +0 -0
  25. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/types/flag.py +0 -0
  26. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/types/int.py +0 -0
  27. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/types/leb128.py +0 -0
  28. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/types/packed.py +0 -0
  29. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/types/pointer.py +0 -0
  30. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/types/structure.py +0 -0
  31. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/types/void.py +0 -0
  32. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect/cstruct/types/wchar.py +0 -0
  33. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect.cstruct.egg-info/SOURCES.txt +0 -0
  34. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect.cstruct.egg-info/dependency_links.txt +0 -0
  35. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect.cstruct.egg-info/entry_points.txt +0 -0
  36. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect.cstruct.egg-info/requires.txt +0 -0
  37. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/dissect.cstruct.egg-info/top_level.txt +0 -0
  38. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/examples/disk.py +0 -0
  39. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/examples/mirai.py +0 -0
  40. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/examples/pe.py +0 -0
  41. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/examples/protobuf.py +0 -0
  42. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/examples/secdesc.py +0 -0
  43. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/pyproject.toml +0 -0
  44. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/setup.cfg +0 -0
  45. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/__init__.py +0 -0
  46. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/_data/testdef.txt +0 -0
  47. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/_docs/Makefile +0 -0
  48. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/_docs/__init__.py +0 -0
  49. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/_docs/conf.py +0 -0
  50. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/_docs/index.rst +0 -0
  51. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/conftest.py +0 -0
  52. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_align.py +0 -0
  53. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_annotations.py +0 -0
  54. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_basic.py +0 -0
  55. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_benchmark.py +0 -0
  56. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_bitbuffer.py +0 -0
  57. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_bitfield.py +0 -0
  58. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_compiler.py +0 -0
  59. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_ctypes.py +0 -0
  60. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_expression.py +0 -0
  61. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_parser.py +0 -0
  62. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_tools_stubgen.py +0 -0
  63. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_types_base.py +0 -0
  64. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_types_char.py +0 -0
  65. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_types_custom.py +0 -0
  66. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_types_enum.py +0 -0
  67. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_types_flag.py +0 -0
  68. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_types_int.py +0 -0
  69. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_types_leb128.py +0 -0
  70. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_types_packed.py +0 -0
  71. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_types_pointer.py +0 -0
  72. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_types_structure.py +0 -0
  73. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_types_union.py +0 -0
  74. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_types_void.py +0 -0
  75. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/test_types_wchar.py +0 -0
  76. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tests/utils.py +0 -0
  77. {dissect_cstruct-4.8.dev6 → dissect_cstruct-4.8.dev8}/tox.ini +0 -0
@@ -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
@@ -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
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
14
14
 
15
15
 
16
16
  def test_hexdump(capsys: pytest.CaptureFixture) -> None:
17
- utils.hexdump(b"\x00" * 16)
17
+ utils.hexdump(b"\x00" * 16, pretty=False)
18
18
  captured = capsys.readouterr()
19
19
  assert captured.out == "00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................\n"
20
20
 
@@ -30,6 +30,137 @@ def test_hexdump(capsys: pytest.CaptureFixture) -> None:
30
30
  utils.hexdump("b\x00", output="str")
31
31
 
32
32
 
33
+ def test_hexdump_pretty(capsys: pytest.CaptureFixture) -> None:
34
+ """Check if we can create a pretty hexdump."""
35
+ c = utils.COLOR_CLEAR
36
+ g = utils.COLOR_GREY
37
+ w = utils.COLOR_WHITE_BOLD
38
+ y = utils.COLOR_YELLOW
39
+
40
+ utils.hexdump((b"\x00" * 5) + b"\x01\x02\x03abc" + (b"\x00" * 5), pretty=True)
41
+ captured = capsys.readouterr()
42
+ assert (
43
+ captured.out
44
+ == "00000000 "
45
+ + (f"{g}00{c} " * 5)
46
+ + f"{y}01{c} {y}02{c} {y}03{c} {w}61{c} {w}62{c} {w}63{c} "
47
+ + (f"{g}00{c} " * 5)
48
+ + " "
49
+ + (f"{g}.{c}" * 5)
50
+ + f"{y}.{c}{y}.{c}{y}.{c}{w}a{c}{w}b{c}{w}c{c}"
51
+ + (f"{g}.{c}" * 5)
52
+ + "\n"
53
+ )
54
+
55
+
56
+ def test_hexdump_autoskip_collapses_middle_null_run() -> None:
57
+ """Keep first interior NUL line, then collapse the rest of that run to '*'."""
58
+ # Layout: [A data line] [3 NUL lines] [B data line]
59
+ # Expected: [A line] [first NUL line] [*] [B line] = 4 lines
60
+ data = (b"A" * 16) + (b"\x00" * 48) + (b"B" * 16)
61
+
62
+ out = utils.hexdump(data, output="string", pretty=False, autoskip=True)
63
+ assert out is not None
64
+
65
+ lines = out.splitlines()
66
+ assert len(lines) == 4
67
+ assert lines[0].startswith("00000000") # A line
68
+ assert lines[1].startswith("00000010") # First NUL line is kept
69
+ assert lines[2] == "*" # Remaining NUL lines collapsed
70
+ assert lines[3].startswith("00000040") # B line
71
+
72
+
73
+ def test_hexdump_autoskip_keeps_edge_null_lines() -> None:
74
+ """Do not collapse first/last hexdump lines even when they are all NUL bytes."""
75
+ # Layout: [3 NUL lines]
76
+ # Expected: all lines are kept (only one interior line, so nothing is repeated there)
77
+ data = b"\x00" * 48
78
+
79
+ out = utils.hexdump(data, output="string", pretty=False, autoskip=True)
80
+ assert out is not None
81
+
82
+ lines = out.splitlines()
83
+ assert len(lines) == 3
84
+ assert lines[0].startswith("00000000") # First line kept (edge)
85
+ assert lines[1].startswith("00000010") # Single interior NUL line is kept
86
+ assert lines[2].startswith("00000020") # Last line kept (edge)
87
+
88
+
89
+ def test_hexdump_autoskip_separate_null_runs() -> None:
90
+ """Emit one '*' per interior NUL run, after keeping each run's first interior NUL line."""
91
+ # Layout: [A data] [2 NUL lines] [B data] [2 NUL lines] [C data]
92
+ # Expected: [A line] [NUL line] [*] [B line] [NUL line] [*] [C line] = 7 lines
93
+ data = (b"A" * 16) + (b"\x00" * 32) + (b"B" * 16) + (b"\x00" * 32) + (b"C" * 16)
94
+
95
+ out = utils.hexdump(data, output="string", pretty=False, autoskip=True)
96
+ assert out is not None
97
+
98
+ lines = out.splitlines()
99
+ assert len(lines) == 7
100
+ assert lines[0].startswith("00000000") # A line
101
+ assert lines[1].startswith("00000010") # First NUL line of first run kept
102
+ assert lines[2] == "*" # Remaining NUL lines of first run collapsed
103
+ assert lines[3].startswith("00000030") # B line
104
+ assert lines[4].startswith("00000040") # First NUL line of second run kept
105
+ assert lines[5] == "*" # Remaining NUL lines of second run collapsed
106
+ assert lines[6].startswith("00000060") # C line
107
+
108
+
109
+ def test_hexdump_autoskip_single_interior_null_line_is_not_collapsed() -> None:
110
+ """Keep a single interior all-NUL line visible when autoskip is enabled."""
111
+ # Layout: [A data line] [1 NUL line] [B data line]
112
+ # Expected: [A line] [NUL line] [B line] = 3 lines (no repeated interior NUL line)
113
+ data = (b"A" * 16) + (b"\x00" * 16) + (b"B" * 16)
114
+
115
+ out = utils.hexdump(data, output="string", pretty=False, autoskip=True)
116
+ assert out is not None
117
+
118
+ lines = out.splitlines()
119
+ assert len(lines) == 3
120
+ assert lines[0].startswith("00000000") # A line
121
+ assert lines[1].startswith("00000010") # Single NUL line is kept
122
+ assert lines[2].startswith("00000020") # B line
123
+
124
+
125
+ def test_hexdump_autoskip_false_does_not_collapse() -> None:
126
+ """Keep all lines expanded and never emit '*' when autoskip is disabled."""
127
+ # Layout: [A data line] [3 NUL lines] [B data line]
128
+ # Expected: [A line] [NUL line] [NUL line] [NUL line] [B line] = 5 lines (no * when disabled)
129
+ data = (b"A" * 16) + (b"\x00" * 48) + (b"B" * 16)
130
+
131
+ out = utils.hexdump(data, output="string", pretty=False, autoskip=False)
132
+ assert out is not None
133
+
134
+ lines = out.splitlines()
135
+ assert len(lines) == 5
136
+ assert all(line != "*" for line in lines) # No * when autoskip disabled
137
+ assert lines[0].startswith("00000000") # A line
138
+ assert lines[1].startswith("00000010") # First NUL line (expanded)
139
+ assert lines[2].startswith("00000020") # Second NUL line (expanded)
140
+ assert lines[3].startswith("00000030") # Third NUL line (expanded)
141
+ assert lines[4].startswith("00000040") # B line
142
+
143
+
144
+ def test_hexdump_pretty_print_conditions(capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch) -> None:
145
+ """Test if we respec the ``NO_COLOR`` environment variable and ``pretty=False`` argument."""
146
+ # Test regular print output behavior
147
+ utils.hexdump(b"\x00" * 16)
148
+ captured = capsys.readouterr()
149
+ assert captured.out.startswith("00000000 \x1b[0;90m00")
150
+
151
+ # Test explicit disable using NO_COLOR
152
+ with monkeypatch.context() as m:
153
+ m.setenv("NO_COLOR", "1")
154
+ utils.hexdump(b"\x00" * 16)
155
+ captured = capsys.readouterr()
156
+ assert captured.out.startswith("00000000 00")
157
+
158
+ # Test explicit disable using pretty=False
159
+ utils.hexdump(b"\x00" * 16, pretty=False)
160
+ captured = capsys.readouterr()
161
+ assert captured.out.startswith("00000000 00")
162
+
163
+
33
164
  def test_dumpstruct(cs: cstruct, capsys: pytest.CaptureFixture, compiled: bool) -> None:
34
165
  cdef = """
35
166
  struct test {