dissect.target 3.13.dev26__py3-none-any.whl → 3.14__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (138) hide show
  1. dissect/target/container.py +9 -1
  2. dissect/target/containers/asdf.py +2 -0
  3. dissect/target/containers/ewf.py +2 -0
  4. dissect/target/containers/hdd.py +2 -0
  5. dissect/target/containers/hds.py +2 -0
  6. dissect/target/containers/qcow2.py +2 -0
  7. dissect/target/containers/raw.py +2 -0
  8. dissect/target/containers/split.py +2 -0
  9. dissect/target/containers/vdi.py +2 -0
  10. dissect/target/containers/vhd.py +2 -0
  11. dissect/target/containers/vhdx.py +2 -0
  12. dissect/target/containers/vmdk.py +2 -0
  13. dissect/target/filesystem.py +108 -15
  14. dissect/target/filesystems/ad1.py +1 -1
  15. dissect/target/filesystems/btrfs.py +180 -0
  16. dissect/target/filesystems/cb.py +4 -4
  17. dissect/target/filesystems/config.py +161 -31
  18. dissect/target/filesystems/dir.py +1 -1
  19. dissect/target/filesystems/exfat.py +1 -1
  20. dissect/target/filesystems/extfs.py +5 -1
  21. dissect/target/filesystems/fat.py +1 -1
  22. dissect/target/filesystems/ffs.py +1 -1
  23. dissect/target/filesystems/itunes.py +1 -1
  24. dissect/target/filesystems/ntfs.py +1 -1
  25. dissect/target/filesystems/smb.py +1 -1
  26. dissect/target/filesystems/squashfs.py +1 -1
  27. dissect/target/filesystems/tar.py +1 -1
  28. dissect/target/filesystems/vmfs.py +1 -1
  29. dissect/target/filesystems/xfs.py +1 -1
  30. dissect/target/filesystems/zip.py +1 -1
  31. dissect/target/helpers/cache.py +2 -2
  32. dissect/target/helpers/configutil.py +283 -83
  33. dissect/target/helpers/fsutil.py +9 -6
  34. dissect/target/helpers/hashutil.py +20 -19
  35. dissect/target/helpers/utils.py +14 -3
  36. dissect/target/loaders/ad1.py +1 -1
  37. dissect/target/loaders/asdf.py +1 -1
  38. dissect/target/loaders/log.py +2 -2
  39. dissect/target/loaders/smb.py +23 -13
  40. dissect/target/loaders/targetd.py +12 -2
  41. dissect/target/loaders/vma.py +1 -1
  42. dissect/target/loaders/xva.py +1 -1
  43. dissect/target/plugin.py +14 -2
  44. dissect/target/plugins/apps/av/sophos.py +1 -2
  45. dissect/target/plugins/apps/av/symantec.py +3 -4
  46. dissect/target/plugins/apps/av/trendmicro.py +2 -3
  47. dissect/target/plugins/{browsers → apps/browser}/chrome.py +6 -3
  48. dissect/target/plugins/{browsers → apps/browser}/chromium.py +18 -13
  49. dissect/target/plugins/{browsers → apps/browser}/edge.py +6 -3
  50. dissect/target/plugins/{browsers → apps/browser}/firefox.py +3 -7
  51. dissect/target/plugins/{browsers → apps/browser}/iexplore.py +14 -4
  52. dissect/target/plugins/apps/remoteaccess/teamviewer.py +55 -27
  53. dissect/target/plugins/apps/ssh/opensshd.py +31 -30
  54. dissect/target/plugins/apps/{webservers → webserver}/apache.py +1 -1
  55. dissect/target/plugins/apps/{webservers → webserver}/caddy.py +1 -1
  56. dissect/target/plugins/apps/{webservers → webserver}/iis.py +1 -1
  57. dissect/target/plugins/apps/{webservers → webserver}/nginx.py +1 -1
  58. dissect/target/plugins/child/hyperv.py +1 -2
  59. dissect/target/plugins/child/vmware_workstation.py +1 -3
  60. dissect/target/plugins/filesystem/acquire_handles.py +2 -0
  61. dissect/target/plugins/filesystem/acquire_hash.py +1 -7
  62. dissect/target/plugins/filesystem/icat.py +5 -5
  63. dissect/target/plugins/filesystem/ntfs/mft.py +2 -2
  64. dissect/target/plugins/filesystem/ntfs/mft_timeline.py +2 -2
  65. dissect/target/plugins/filesystem/ntfs/usnjrnl.py +2 -3
  66. dissect/target/plugins/filesystem/resolver.py +1 -1
  67. dissect/target/plugins/filesystem/unix/capability.py +77 -66
  68. dissect/target/plugins/filesystem/walkfs.py +25 -19
  69. dissect/target/plugins/filesystem/yara.py +20 -19
  70. dissect/target/plugins/general/config.py +28 -11
  71. dissect/target/plugins/os/unix/_os.py +28 -21
  72. dissect/target/plugins/os/unix/bsd/osx/user.py +1 -3
  73. dissect/target/plugins/os/unix/cronjobs.py +4 -16
  74. dissect/target/plugins/os/unix/{linux/esxi → esxi}/_os.py +5 -6
  75. dissect/target/plugins/os/unix/generic.py +5 -1
  76. dissect/target/plugins/os/unix/history.py +2 -1
  77. dissect/target/plugins/os/unix/linux/_os.py +12 -5
  78. dissect/target/plugins/os/unix/linux/services.py +112 -0
  79. dissect/target/plugins/os/unix/linux/suse/zypper.py +4 -4
  80. dissect/target/plugins/os/unix/locale.py +3 -1
  81. dissect/target/plugins/os/unix/log/journal.py +7 -6
  82. dissect/target/plugins/os/unix/packagemanager.py +3 -3
  83. dissect/target/plugins/os/unix/shadow.py +1 -1
  84. dissect/target/plugins/os/windows/_os.py +2 -1
  85. dissect/target/plugins/os/windows/amcache.py +9 -10
  86. dissect/target/plugins/os/windows/catroot.py +2 -2
  87. dissect/target/plugins/os/windows/cim.py +5 -4
  88. dissect/target/plugins/os/windows/datetime.py +4 -1
  89. dissect/target/plugins/os/windows/defender.py +3 -3
  90. dissect/target/plugins/os/windows/generic.py +10 -11
  91. dissect/target/plugins/os/windows/lnk.py +6 -6
  92. dissect/target/plugins/os/windows/log/amcache.py +3 -5
  93. dissect/target/plugins/os/windows/log/pfro.py +1 -3
  94. dissect/target/plugins/os/windows/prefetch.py +5 -6
  95. dissect/target/plugins/os/windows/recyclebin.py +3 -4
  96. dissect/target/plugins/os/windows/regf/7zip.py +2 -4
  97. dissect/target/plugins/os/windows/regf/bam.py +1 -2
  98. dissect/target/plugins/os/windows/regf/cit.py +4 -5
  99. dissect/target/plugins/os/windows/regf/mru.py +6 -2
  100. dissect/target/plugins/os/windows/regf/muicache.py +1 -3
  101. dissect/target/plugins/os/windows/regf/recentfilecache.py +1 -2
  102. dissect/target/plugins/os/windows/regf/shimcache.py +1 -2
  103. dissect/target/plugins/os/windows/regf/trusteddocs.py +1 -1
  104. dissect/target/plugins/os/windows/regf/userassist.py +1 -2
  105. dissect/target/plugins/os/windows/services.py +2 -4
  106. dissect/target/plugins/os/windows/sru.py +4 -4
  107. dissect/target/plugins/os/windows/startupinfo.py +5 -6
  108. dissect/target/plugins/os/windows/syscache.py +2 -3
  109. dissect/target/target.py +65 -32
  110. dissect/target/tools/info.py +2 -1
  111. dissect/target/tools/mount.py +2 -12
  112. dissect/target/tools/shell.py +3 -2
  113. dissect/target/volume.py +10 -9
  114. dissect/target/volumes/bde.py +1 -1
  115. dissect/target/volumes/ddf.py +2 -0
  116. dissect/target/volumes/disk.py +2 -0
  117. dissect/target/volumes/luks.py +1 -1
  118. dissect/target/volumes/lvm.py +2 -0
  119. dissect/target/volumes/md.py +2 -0
  120. dissect/target/volumes/vmfs.py +2 -0
  121. {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/METADATA +2 -1
  122. {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/RECORD +137 -136
  123. {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/WHEEL +1 -1
  124. dissect/target/plugins/os/unix/services.py +0 -151
  125. /dissect/target/plugins/apps/{containers → browser}/__init__.py +0 -0
  126. /dissect/target/plugins/{browsers → apps/browser}/browser.py +0 -0
  127. /dissect/target/plugins/apps/{vpns → container}/__init__.py +0 -0
  128. /dissect/target/plugins/apps/{containers → container}/docker.py +0 -0
  129. /dissect/target/plugins/apps/{webservers → vpn}/__init__.py +0 -0
  130. /dissect/target/plugins/apps/{vpns → vpn}/openvpn.py +0 -0
  131. /dissect/target/plugins/apps/{vpns → vpn}/wireguard.py +0 -0
  132. /dissect/target/plugins/{browsers → apps/webserver}/__init__.py +0 -0
  133. /dissect/target/plugins/apps/{webservers/webservers.py → webserver/webserver.py} +0 -0
  134. /dissect/target/plugins/os/unix/{linux/esxi → esxi}/__init__.py +0 -0
  135. {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/COPYRIGHT +0 -0
  136. {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/LICENSE +0 -0
  137. {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/entry_points.txt +0 -0
  138. {dissect.target-3.13.dev26.dist-info → dissect.target-3.14.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,19 @@ from collections import deque
6
6
  from configparser import ConfigParser, MissingSectionHeaderError
7
7
  from dataclasses import dataclass
8
8
  from fnmatch import fnmatch
9
- from typing import Any, ItemsView, Iterator, KeysView, Optional, TextIO, Union
9
+ from types import TracebackType
10
+ from typing import (
11
+ Any,
12
+ Callable,
13
+ ItemsView,
14
+ Iterable,
15
+ Iterator,
16
+ KeysView,
17
+ Literal,
18
+ Optional,
19
+ TextIO,
20
+ Union,
21
+ )
10
22
 
11
23
  from dissect.target.exceptions import ConfigurationParsingError, FileNotFoundError
12
24
  from dissect.target.filesystem import FilesystemEntry
@@ -58,18 +70,32 @@ class PeekableIterator:
58
70
 
59
71
 
60
72
  class ConfigurationParser:
73
+ """A configuration parser where you can configure certain aspects of the parsing mechanism.
74
+
75
+ Attributes:
76
+ parsed_data: The resulting dictionary after parsing.
77
+
78
+ Args:
79
+ collapse: A ``bool`` or an ``Iterator``:
80
+ If ``True``: it will collapse all the resulting dictionary values.
81
+ If an ``Iterable`` it will collapse on the keys defined in ``collapse``.
82
+ collapse_inverse: Inverses the collapsing mechanism. Collapse on everything that is not inside ``collapse``.
83
+ separator: Contains what values it should look for as a separator.
84
+ comment_prefixes: Contains what constitutes as a comment.
85
+ """
86
+
61
87
  def __init__(
62
88
  self,
63
- collapse: Union[bool, set] = False,
89
+ collapse: Union[bool, Iterable[str]] = False,
64
90
  collapse_inverse: bool = False,
65
- seperator: tuple[str] = ("=",),
91
+ separator: tuple[str] = ("=",),
66
92
  comment_prefixes: tuple[str] = (";", "#"),
67
93
  ) -> None:
68
94
  self.collapse_all = collapse is True
69
- self.collapse = collapse if isinstance(collapse, set) else set()
95
+ self.collapse = set(collapse) if isinstance(collapse, Iterable) else set()
70
96
  self._collapse_check = self._key_not_in_collapse if collapse_inverse else self._key_in_collapse
71
97
 
72
- self.seperator = seperator
98
+ self.separator = separator
73
99
  self.comment_prefixes = comment_prefixes
74
100
  self.parsed_data = {}
75
101
 
@@ -101,6 +127,13 @@ class ConfigurationParser:
101
127
  return key not in self.collapse
102
128
 
103
129
  def parse_file(self, fh: TextIO) -> None:
130
+ """Parse the contents of ``fh`` into key/value pairs.
131
+
132
+ This function should **set** :attr:`parsed_data` as a side_effect.
133
+
134
+ Args:
135
+ fh: The text to parse.
136
+ """
104
137
  raise NotImplementedError()
105
138
 
106
139
  def get(self, item: str, default: Optional[Any] = None) -> Any:
@@ -110,7 +143,7 @@ class ConfigurationParser:
110
143
  """Parse a configuration file.
111
144
 
112
145
  Raises:
113
- ConfigurationParsingError: If any exception occurs during during the parsing process
146
+ ConfigurationParsingError: If any exception occurs during during the parsing process.
114
147
  """
115
148
 
116
149
  try:
@@ -121,6 +154,9 @@ class ConfigurationParser:
121
154
  if self.collapse_all or self.collapse:
122
155
  self.parsed_data = self._collapse_dict(self.parsed_data)
123
156
 
157
+ if not isinstance(self.parsed_data, dict):
158
+ self.parsed_data = self._collapse_dict(self.parsed_data, False)
159
+
124
160
  def keys(self) -> KeysView:
125
161
  return self.parsed_data.keys()
126
162
 
@@ -129,19 +165,19 @@ class ConfigurationParser:
129
165
 
130
166
 
131
167
  class Default(ConfigurationParser):
132
- """Parse a configuration file specified by ``seperator`` and ``comment_prefixes``.
168
+ """Parse a configuration file specified by ``separator`` and ``comment_prefixes``.
133
169
 
134
- This parser splits only on the first ``seperator`` it finds:
170
+ This parser splits only on the first ``separator`` it finds:
135
171
 
136
- key<seperator>value -> {"key": "value"}
172
+ key<separator>value -> {"key": "value"}
137
173
 
138
- key<seperator>value\n
174
+ key<separator>value\n
139
175
  continuation
140
176
  -> {"key": "value continuation"}
141
177
 
142
178
  # Unless we collapse values, we add them to a list to not overwrite any values.
143
- key<seperator>value1
144
- key<seperator>value2
179
+ key<separator>value1
180
+ key<separator>value2
145
181
  -> {key: [value1, value2]}
146
182
 
147
183
  <empty_space><comment> -> skip
@@ -149,17 +185,18 @@ class Default(ConfigurationParser):
149
185
 
150
186
  def __init__(self, *args, **kwargs) -> None:
151
187
  super().__init__(*args, **kwargs)
152
- self.SEPERATOR = re.compile(rf"\s*[{''.join(self.seperator)}]\s*")
188
+ self.SEPARATOR = re.compile(rf"\s*[{''.join(self.separator)}]\s*")
153
189
  self.COMMENTS = re.compile(rf"\s*[{''.join(self.comment_prefixes)}]")
154
190
  self.skip_lines = self.comment_prefixes + ("\n",)
155
191
 
156
- def line_reader(self, fh: TextIO) -> Iterator[str]:
192
+ def line_reader(self, fh: TextIO, strip_comments: bool = True) -> Iterator[str]:
157
193
  for line in fh:
158
194
  if line.strip().startswith(self.skip_lines) or not line.strip():
159
195
  continue
160
196
 
161
- # Strip the comments first
162
- line, *_ = self.COMMENTS.split(line, 1)
197
+ if strip_comments:
198
+ line, *_ = self.COMMENTS.split(line, 1)
199
+
163
200
  yield line
164
201
 
165
202
  def parse_file(self, fh: TextIO) -> None:
@@ -173,7 +210,7 @@ class Default(ConfigurationParser):
173
210
  information_dict[prev_key] = " ".join([prev_value, line.strip()])
174
211
  continue
175
212
 
176
- prev_key, *value = self.SEPERATOR.split(line, 1)
213
+ prev_key, *value = self.SEPARATOR.split(line, 1)
177
214
  value = value[0].strip() if value else ""
178
215
 
179
216
  _update_dictionary(information_dict, prev_key, value)
@@ -189,7 +226,7 @@ class Ini(ConfigurationParser):
189
226
 
190
227
  self.parsed_data = ConfigParser(
191
228
  strict=False,
192
- delimiters=self.seperator,
229
+ delimiters=self.separator,
193
230
  comment_prefixes=self.comment_prefixes,
194
231
  allow_no_value=True,
195
232
  interpolation=None,
@@ -217,11 +254,86 @@ class Txt(ConfigurationParser):
217
254
  self.parsed_data = {"content": fh.read(), "size": str(fh.tell())}
218
255
 
219
256
 
257
+ class ScopeManager:
258
+ """A (context)manager for dictionary scoping.
259
+
260
+ This class provides utility functions to keep track of scopes inside a dictionary.
261
+
262
+ Attributes:
263
+ _parents: A dictionary accounting what child belongs to which parent dictionary.
264
+ _root: The initial dictionary.
265
+ _current: The current dictionary.
266
+ _previous: The node before the current (changed) node.
267
+ """
268
+
269
+ def __init__(self) -> None:
270
+ self._parents = {}
271
+ self._root = {}
272
+ self._current = self._root
273
+ self._previous = None
274
+
275
+ def __enter__(self) -> ScopeManager:
276
+ return self
277
+
278
+ def __exit__(
279
+ self,
280
+ type: Optional[type[BaseException]],
281
+ value: Optional[BaseException],
282
+ traceback: Optional[TracebackType],
283
+ ) -> None:
284
+ self.clean()
285
+
286
+ def _set_prev(self, keep_prev: bool) -> None:
287
+ """Set :attr:`_previous` before :attr:`_current` changes."""
288
+ if not keep_prev:
289
+ self._previous = self._current
290
+
291
+ def push(self, name: str, keep_prev: bool = False) -> Literal[True]:
292
+ """Push a new key to the :attr:`_current` dictionary and return that we did."""
293
+ child = self._current.get(name, {})
294
+
295
+ parent = self._current
296
+ self._parents[id(child)] = parent
297
+ parent[name] = child
298
+ self._set_prev(keep_prev)
299
+ self._current = child
300
+ return True
301
+
302
+ def pop(self, keep_prev: bool = False) -> bool:
303
+ """Pop :attr:`_current` and return whether we changed the :attr:`_parents` dictionary."""
304
+ if new_current := self._parents.pop(id(self._current), None):
305
+ self._set_prev(keep_prev)
306
+ self._current = new_current
307
+ return True
308
+ return False
309
+
310
+ def update(self, key: str, value: str) -> None:
311
+ """Update the :attr:`_current` dictionary with ``key`` and ``value``."""
312
+ _update_dictionary(self._current, key, value)
313
+
314
+ def update_prev(self, key: str, value: str) -> None:
315
+ """Update the :attr:`_previous` dictionary with ``key`` and ``value``."""
316
+ _update_dictionary(self._previous, key, value)
317
+
318
+ def is_root(self) -> bool:
319
+ """Utility function to check whether the current dictionary is a root dictionary."""
320
+ return id(self._current) == id(self._root)
321
+
322
+ def clean(self) -> None:
323
+ """Clean up the internal state.
324
+ This is called automatically when :class:`ScopeManager` is used as a contextmanager.
325
+ """
326
+ self._parents = {}
327
+ self._root = {}
328
+ self._current = self._root
329
+ self._previous = None
330
+
331
+
220
332
  class Indentation(Default):
221
- """This parser is used for the files that use a single level of indentation to specify a different scope.
333
+ """This parser is used for files that use a single level of indentation to specify a different scope.
222
334
 
223
- Examples of these files are for example the sshd_config file.
224
- Where "Match" statments use a single layer of indentaiton to specify a scope for the key value pairs.
335
+ Examples of these files are the ``sshd_config`` file.
336
+ Where "Match" statements use a single layer of indentation to specify a scope for the key value pairs.
225
337
 
226
338
  The parser parses this as the following:
227
339
 
@@ -230,80 +342,166 @@ class Indentation(Default):
230
342
  -> {"key value": {"key2": "value2"}}
231
343
  """
232
344
 
233
- def __init__(self, *args, **kwargs) -> None:
234
- super().__init__(*args, **kwargs)
235
- self._parents = {}
236
- self._indentation = 0
237
-
238
345
  def _parse_line(self, line: str) -> tuple[str, str]:
239
- key, *value = self.SEPERATOR.split(line.strip(), 1)
346
+ key, *value = self.SEPARATOR.split(line.strip(), 1)
240
347
  value = value[0].strip() if value else ""
241
348
  return key, value
242
349
 
243
- def _push_scope(self, name: str, current: dict[str, Union[str, dict]]) -> dict[str, Union[str, dict]]:
244
- child = current.get(name, {})
245
-
246
- parent = current
247
- self._parents[id(child)] = parent
248
- parent[name] = child
249
- return child
250
-
251
- def _pop_scope(self, current: dict[str, Union[str, dict]]) -> dict[str, Union[str, dict]]:
252
- self._indentation = 0
253
- return self._parents.pop(id(current), current)
254
-
255
350
  def _change_scope(
256
351
  self,
352
+ manager: ScopeManager,
257
353
  line: str,
258
- next_line: Optional[str],
259
- key: Optional[str],
260
- current: dict[str, Union[str, dict]],
261
- ) -> dict[str, Union[str, dict]]:
354
+ key: str,
355
+ next_line: Optional[str] = None,
356
+ ) -> bool:
357
+ """A function to check whether to create a new scope, or go back to a previous one.
358
+
359
+ Args:
360
+ manager: A :class:`ScopeManager` that contains the logic to ``push`` and ``pop`` scopes. And keeps state.
361
+ line: The line to be parsed.
362
+ key: The key that should be updated during a :method:`ScopeManager.push``.
363
+ next_line: The next line to be parsed.
364
+
365
+ Returns:
366
+ Whether the scope changed or not.
367
+ """
262
368
  empty_space = (" ", "\t")
369
+ changed = False
263
370
 
264
371
  if next_line is None:
265
- return current
372
+ return False
266
373
 
267
374
  if not line.startswith(empty_space):
268
- current = self._pop_scope(current)
375
+ changed = manager.pop()
269
376
 
270
377
  if not line.startswith(empty_space) and next_line.startswith(empty_space):
271
- self._indentation = len(next_line) - len(next_line.lstrip())
272
- return self._push_scope(key, current)
273
- return current
378
+ return manager.push(key)
379
+ return changed
274
380
 
275
381
  def parse_file(self, fh: TextIO) -> None:
276
- root = {}
277
- current = root
278
-
279
382
  iterator = PeekableIterator(self.line_reader(fh))
280
383
  prev_key = None
281
- for line in iterator:
282
- key, value = self._parse_line(line)
283
- prev_dict = current
284
- current = self._change_scope(line, iterator.peek(), line.strip(), current)
285
384
 
286
- if id(current) != id(prev_dict):
287
- prev_key = line.strip()
288
- continue
385
+ with ScopeManager() as manager:
386
+ for line in iterator:
387
+ key, value = self._parse_line(line)
388
+ changed = self._change_scope(
389
+ manager=manager,
390
+ line=line,
391
+ key=line.strip(),
392
+ next_line=iterator.peek(),
393
+ )
394
+
395
+ if changed:
396
+ prev_key = line.strip()
397
+ continue
398
+
399
+ if not value:
400
+ key, value = prev_key, key
401
+ manager.pop()
402
+
403
+ manager.update(key, value)
404
+
405
+ self.parsed_data = manager._root
406
+
407
+
408
+ class SystemD(Indentation):
409
+ """A :class:`ConfigurationParser` that specifically parses systemd configuration files.
410
+
411
+ Examples:
412
+ >>> systemd_data = textwrap.dedent(
413
+ '''
414
+ [Section1]
415
+ Key=Value
416
+ [Section2]
417
+ Key2=Value 2\\
418
+ Value 2 continued
419
+ '''
420
+ )
421
+ >>> parser = SystemD(io.StringIO(systemd_data))
422
+ >>> parser.parser_items
423
+ {
424
+ "Section1": {
425
+ "Key": "Value
426
+ },
427
+ "Section2": {
428
+ "Key2": "Value2 Value 2 continued
429
+ }
430
+ }
431
+
432
+ """
289
433
 
290
- if not value:
291
- key, value = prev_key, key
292
- current = self._pop_scope(current)
434
+ def _change_scope(
435
+ self,
436
+ manager: ScopeManager,
437
+ line: str,
438
+ key: str,
439
+ next_line: Optional[str] = None,
440
+ ) -> bool:
441
+ scope_char = ("[", "]")
442
+ changed = False
443
+ if line.startswith(scope_char):
444
+ if not manager.is_root():
445
+ changed = manager.pop()
446
+ stripped_characters = "".join(scope_char)
447
+ changed = manager.push(key.strip(stripped_characters), changed)
448
+
449
+ return changed
293
450
 
294
- _update_dictionary(current, key, value)
451
+ def parse_file(self, fh: TextIO) -> None:
452
+ prev_values = []
453
+ prev_key = None
295
454
 
296
- self.parsed_data = root
297
- # Cleanup of internal state
298
- self._parents = {}
299
- self._indentation = 0
455
+ with ScopeManager() as manager:
456
+ for line in self.line_reader(fh, strip_comments=False):
457
+ changed = self._change_scope(
458
+ manager=manager,
459
+ line=line,
460
+ key=line.strip(),
461
+ )
462
+
463
+ if changed:
464
+ # Current part is a section header.
465
+ if prev_values:
466
+ # Update previous key/value... someone configured it wrong
467
+ prev_values, prev_key = self._update_continued_values(
468
+ func=manager.update_prev,
469
+ key=prev_key,
470
+ values=prev_values,
471
+ )
472
+ continue
473
+
474
+ key, value = self._parse_line(line)
475
+
476
+ continued_value = value or key
477
+ if continued_value.endswith("\\"):
478
+ prev_key = prev_key or key
479
+ prev_values.append(continued_value.strip("\\ "))
480
+ continue
481
+
482
+ if prev_values:
483
+ prev_values, prev_key = self._update_continued_values(
484
+ func=manager.update,
485
+ key=prev_key,
486
+ values=prev_values + [continued_value],
487
+ )
488
+ continue
489
+
490
+ manager.update(key, value)
491
+
492
+ self.parsed_data = manager._root
493
+
494
+ def _update_continued_values(self, func: Callable, key, values: list[str]) -> tuple[list, None]:
495
+ value = " ".join(values)
496
+ func(key, value)
497
+ return [], None
300
498
 
301
499
 
302
500
  @dataclass(frozen=True)
303
501
  class ParserOptions:
304
502
  collapse: Optional[Union[bool, set]] = None
305
503
  collapse_inverse: Optional[bool] = None
306
- seperator: Optional[tuple[str]] = None
504
+ separator: Optional[tuple[str]] = None
307
505
  comment_prefixes: Optional[tuple[str]] = None
308
506
 
309
507
 
@@ -312,13 +510,13 @@ class ParserConfig:
312
510
  parser: type[ConfigurationParser] = Default
313
511
  collapse: Optional[Union[bool, set]] = None
314
512
  collapse_inverse: Optional[bool] = None
315
- seperator: Optional[tuple[str]] = None
513
+ separator: Optional[tuple[str]] = None
316
514
  comment_prefixes: Optional[tuple[str]] = None
317
515
 
318
516
  def create_parser(self, options: Optional[ParserOptions] = None) -> ConfigurationParser:
319
517
  kwargs = {}
320
518
 
321
- for field_name in ["collapse", "collapse_inverse", "seperator", "comment_prefixes"]:
519
+ for field_name in ["collapse", "collapse_inverse", "separator", "comment_prefixes"]:
322
520
  value = getattr(options, field_name, None) or getattr(self, field_name)
323
521
  if value:
324
522
  kwargs.update({field_name: value})
@@ -327,28 +525,29 @@ class ParserConfig:
327
525
 
328
526
 
329
527
  MATCH_MAP: dict[str, ParserConfig] = {
330
- "*/systemd/*": ParserConfig(Ini),
528
+ "*/systemd/*": ParserConfig(SystemD),
331
529
  "*/sysconfig/network-scripts/ifcfg-*": ParserConfig(Default),
332
530
  "*/sysctl.d/*.conf": ParserConfig(Default),
333
531
  }
334
532
 
335
-
336
533
  CONFIG_MAP: dict[tuple[str, ...], ParserConfig] = {
337
534
  "ini": ParserConfig(Ini),
338
535
  "xml": ParserConfig(Txt),
339
536
  "json": ParserConfig(Txt),
340
537
  "cnf": ParserConfig(Default),
341
- "conf": ParserConfig(Default, seperator=(r"\s")),
538
+ "conf": ParserConfig(Default, separator=(r"\s",)),
342
539
  "sample": ParserConfig(Txt),
540
+ "systemd": ParserConfig(SystemD),
343
541
  "template": ParserConfig(Txt),
344
542
  }
543
+
345
544
  KNOWN_FILES: dict[str, type[ConfigurationParser]] = {
346
545
  "ulogd.conf": ParserConfig(Ini),
347
- "sshd_config": ParserConfig(Indentation, seperator=(r"\s",)),
348
- "hosts.allow": ParserConfig(Default, seperator=(":",), comment_prefixes=("#",)),
349
- "hosts.deny": ParserConfig(Default, seperator=(":",), comment_prefixes=("#",)),
350
- "hosts": ParserConfig(Default, seperator=(r"\s")),
351
- "nsswitch.conf": ParserConfig(Default, seperator=(":",)),
546
+ "sshd_config": ParserConfig(Indentation, separator=(r"\s",)),
547
+ "hosts.allow": ParserConfig(Default, separator=(":",), comment_prefixes=("#",)),
548
+ "hosts.deny": ParserConfig(Default, separator=(":",), comment_prefixes=("#",)),
549
+ "hosts": ParserConfig(Default, separator=(r"\s",)),
550
+ "nsswitch.conf": ParserConfig(Default, separator=(":",)),
352
551
  "lsb-release": ParserConfig(Default),
353
552
  }
354
553
 
@@ -357,17 +556,18 @@ def parse(path: Union[FilesystemEntry, TargetPath], hint: Optional[str] = None,
357
556
  """Parses the content of an ``path`` or ``entry`` to a dictionary.
358
557
 
359
558
  Args:
360
- file_path: An entry or targetpath that with contents to parse
361
- hint: A hint to what parser should be used.
362
- collapse:
363
- seperator: What seperator to use for key value mapping
364
- comment_prefixes: The characters that determine a comment.
559
+ path: The path to either a directory or file.
560
+ hint: What kind of parser should be used.
561
+ collapse: Whether it should collapse everything or just a certain set of keys.
562
+ collapse_inverse: Invert the collapse function to collapse everything but the keys inside ``collapse``.
563
+ separator: The separator that should be used for parsing.
564
+ comment_prefixes: What is specified as a comment.
365
565
 
366
566
  Raises:
367
567
  FileNotFoundError: If the ``path`` is not a file.
368
568
  """
369
569
 
370
- if not path.is_file():
570
+ if not path.is_file(follow_symlinks=True):
371
571
  raise FileNotFoundError(f"Could not parse {path} as a dictionary.")
372
572
 
373
573
  entry = path
@@ -725,12 +725,15 @@ class TargetPath(Path, PureDissectPath):
725
725
 
726
726
  def open(self, mode='rb', buffering=0, encoding=None,
727
727
  errors=None, newline=None):
728
- # CPython >= 3.10
729
- if "b" not in mode and hasattr(io, "text_encoding"):
730
- # Vermin linting needs to be skipped for this line as this is
731
- # guarded by an explicit check for availability.
732
- # novermin
733
- encoding = io.text_encoding(encoding)
728
+
729
+ if "b" not in mode:
730
+ encoding = encoding or "UTF-8"
731
+ # CPython >= 3.10
732
+ if hasattr(io, "text_encoding"):
733
+ # Vermin linting needs to be skipped for this line as this is
734
+ # guarded by an explicit check for availability.
735
+ # novermin
736
+ encoding = io.text_encoding(encoding)
734
737
  return self._accessor.open(self, mode, buffering, encoding, errors,
735
738
  newline)
736
739
 
@@ -15,13 +15,9 @@ if TYPE_CHECKING:
15
15
 
16
16
  BUFFER_SIZE = 32768
17
17
 
18
- HashRecord = RecordDescriptor(
19
- "filesystem/file/digest",
20
- [
21
- ("path[]", "paths"),
22
- ("digest[]", "digests"),
23
- ],
24
- )
18
+ RECORD_NAME = "filesystem/file/digest"
19
+ NAME_SUFFIXES = ["_resolved", "_digest"]
20
+ RECORD_TYPES = ["path", "digest"]
25
21
 
26
22
 
27
23
  def _hash(fh: BinaryIO, ctx: Union[HASH, list[HASH]]) -> tuple[str]:
@@ -76,12 +72,8 @@ def hash_uri_records(target: Target, record: Record) -> Record:
76
72
 
77
73
  def hash_path_records(target: Target, record: Record) -> Record:
78
74
  """Hash files from path fields inside the record."""
79
- hashed_paths = []
80
75
 
81
- if target.os == "windows":
82
- path_type = fieldtypes.windows_path
83
- else:
84
- path_type = fieldtypes.posix_path
76
+ hash_records = []
85
77
 
86
78
  for field_name, field_type in record._field_types.items():
87
79
  if not issubclass(field_type, fieldtypes.path):
@@ -97,16 +89,25 @@ def hash_path_records(target: Target, record: Record) -> Record:
97
89
  except (FileNotFoundError, IsADirectoryError):
98
90
  pass
99
91
  else:
100
- resolved_path = path_type(resolved_path)
101
- hashed_paths.append((resolved_path, path_hash))
92
+ resolved_path = target.fs.path(resolved_path)
93
+ record_kwargs = dict()
94
+ record_def = list()
102
95
 
103
- if not hashed_paths:
104
- return record
96
+ fields = [resolved_path, path_hash]
97
+
98
+ for type, name, field in zip(RECORD_TYPES, NAME_SUFFIXES, fields):
99
+ hashed_field_name = f"{field_name}{name}"
100
+ record_kwargs.update({hashed_field_name: field})
101
+ record_def.append((type, hashed_field_name))
105
102
 
106
- paths, digests = zip(*hashed_paths)
107
- hash_record = HashRecord(paths=paths, digests=digests)
103
+ _record = RecordDescriptor(RECORD_NAME, record_def)
104
+
105
+ hash_records.append(_record(**record_kwargs))
106
+
107
+ if not hash_records:
108
+ return record
108
109
 
109
- return GroupedRecord(record._desc.name, [record, hash_record])
110
+ return GroupedRecord(record._desc.name, [record] + hash_records)
110
111
 
111
112
 
112
113
  def hash_uri(target: Target, path: str) -> tuple[str, str]:
@@ -4,7 +4,7 @@ import urllib.parse
4
4
  from datetime import datetime, timezone, tzinfo
5
5
  from enum import Enum
6
6
  from pathlib import Path
7
- from typing import BinaryIO, Iterator, Union
7
+ from typing import BinaryIO, Callable, Iterator, Optional, Union
8
8
 
9
9
  from dissect.util.ts import from_unix
10
10
 
@@ -17,7 +17,7 @@ class StrEnum(str, Enum):
17
17
  """Sortable and serializible string-based enum"""
18
18
 
19
19
 
20
- def list_to_frozen_set(function):
20
+ def list_to_frozen_set(function: Callable) -> Callable:
21
21
  def wrapper(*args):
22
22
  args = [frozenset(x) if isinstance(x, list) else x for x in args]
23
23
  return function(*args)
@@ -25,7 +25,7 @@ def list_to_frozen_set(function):
25
25
  return wrapper
26
26
 
27
27
 
28
- def parse_path_uri(path):
28
+ def parse_path_uri(path: Path) -> tuple[Optional[str], Optional[str], Optional[str]]:
29
29
  if path is None:
30
30
  return None, None, None
31
31
  parsed_path = urllib.parse.urlparse(str(path))
@@ -33,6 +33,17 @@ def parse_path_uri(path):
33
33
  return parsed_path.scheme, parsed_path.path, parsed_query
34
34
 
35
35
 
36
+ def parse_options_string(options: str) -> dict[str, Union[str, bool]]:
37
+ result = {}
38
+ for opt in options.split(","):
39
+ if "=" in opt:
40
+ key, _, value = opt.partition("=")
41
+ result[key] = value
42
+ else:
43
+ result[opt] = True
44
+ return result
45
+
46
+
36
47
  SLUG_RE = re.compile(r"[/\\ ]")
37
48
 
38
49
 
@@ -11,7 +11,7 @@ class AD1Loader(Loader):
11
11
  path = path.resolve()
12
12
 
13
13
  super().__init__(path)
14
- self.ad1 = ad1.AD1(open(path, "rb"))
14
+ self.ad1 = ad1.AD1(path.open("rb"))
15
15
 
16
16
  @staticmethod
17
17
  def detect(path):