dissect.target 3.19.dev29__py3-none-any.whl → 3.19.dev31__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.
@@ -3,11 +3,11 @@ from __future__ import annotations
3
3
  import io
4
4
  import textwrap
5
5
  from logging import getLogger
6
- from typing import Any, BinaryIO, Iterator, Optional, Union
6
+ from typing import Any, BinaryIO, Iterator, Optional
7
7
 
8
8
  from dissect.target import Target
9
9
  from dissect.target.exceptions import ConfigurationParsingError, FileNotFoundError
10
- from dissect.target.filesystem import Filesystem, FilesystemEntry, VirtualFilesystem
10
+ from dissect.target.filesystem import FilesystemEntry, VirtualFilesystem
11
11
  from dissect.target.helpers import fsutil
12
12
  from dissect.target.helpers.configutil import ConfigurationParser, parse
13
13
 
@@ -46,7 +46,7 @@ class ConfigurationFilesystem(VirtualFilesystem):
46
46
  super().__init__(**kwargs)
47
47
  self.root.top = target.fs.get(path)
48
48
 
49
- def _get_till_file(self, path: str, relentry: FilesystemEntry) -> tuple[list[str], FilesystemEntry]:
49
+ def _get_till_file(self, path: str, relentry: FilesystemEntry | None) -> tuple[list[str], FilesystemEntry]:
50
50
  """Searches for the file entry that is pointed to by ``path``.
51
51
 
52
52
  The ``path`` could contain ``key`` entries too, so it searches for the entry from
@@ -56,9 +56,13 @@ class ConfigurationFilesystem(VirtualFilesystem):
56
56
  A list of ``parts`` containing keys: [keys, into, the, file].
57
57
  And the resolved entry: Entry(filename)
58
58
  """
59
+
59
60
  entry = relentry or self.root
61
+ root_path = relentry.path if relentry else self.root.top.path
60
62
 
61
- path = fsutil.normalize(path, alt_separator=self.alt_separator).strip("/")
63
+ # Calculate the relative path
64
+ relpath = fsutil.relpath(path, root_path, alt_separator=self.alt_separator)
65
+ path = fsutil.normalize(relpath, alt_separator=self.alt_separator).strip("/")
62
66
 
63
67
  if not path:
64
68
  return [], entry
@@ -85,10 +89,8 @@ class ConfigurationFilesystem(VirtualFilesystem):
85
89
 
86
90
  return parts[idx:], entry
87
91
 
88
- def get(
89
- self, path: str, relentry: Optional[FilesystemEntry] = None, *args, **kwargs
90
- ) -> Union[FilesystemEntry, ConfigurationEntry]:
91
- """Retrieve a :class:`ConfigurationEntry` or :class:`.FilesystemEntry` relative to the root or ``relentry``.
92
+ def get(self, path: str, relentry: Optional[FilesystemEntry] = None, *args, **kwargs) -> ConfigurationEntry:
93
+ """Retrieve a :class:`ConfigurationEntry` relative to the root or ``relentry``.
92
94
 
93
95
  Raises:
94
96
  FileNotFoundError: if it could not find the entry.
@@ -96,7 +98,7 @@ class ConfigurationFilesystem(VirtualFilesystem):
96
98
  parts, entry = self._get_till_file(path, relentry)
97
99
 
98
100
  if entry.is_dir():
99
- return entry
101
+ return ConfigurationEntry(self, entry.path, entry, None)
100
102
 
101
103
  entry = self._convert_entry(entry, *args, **kwargs)
102
104
 
@@ -108,23 +110,21 @@ class ConfigurationFilesystem(VirtualFilesystem):
108
110
 
109
111
  return entry
110
112
 
111
- def _convert_entry(
112
- self, file_entry: FilesystemEntry, *args, **kwargs
113
- ) -> Union[ConfigurationEntry, FilesystemEntry]:
113
+ def _convert_entry(self, file_entry: FilesystemEntry, *args, **kwargs) -> ConfigurationEntry:
114
114
  """Creates a :class:`ConfigurationEntry` from a ``file_entry``.
115
115
 
116
116
  If an error occurs during the parsing of the file contents,
117
117
  the original ``file_entry`` is returned.
118
118
  """
119
119
  entry = file_entry
120
+ config_parser = None
120
121
  try:
121
122
  config_parser = parse(entry, *args, **kwargs)
122
- entry = ConfigurationEntry(self, entry.path, entry, config_parser)
123
123
  except ConfigurationParsingError as e:
124
124
  # If a parsing error gets created, it should return the `entry`
125
125
  log.debug("Error when parsing %s with message '%s'", entry.path, e)
126
126
 
127
- return entry
127
+ return ConfigurationEntry(self, entry.path, entry, config_parser)
128
128
 
129
129
 
130
130
  class ConfigurationEntry(FilesystemEntry):
@@ -163,10 +163,10 @@ class ConfigurationEntry(FilesystemEntry):
163
163
 
164
164
  def __init__(
165
165
  self,
166
- fs: Filesystem,
166
+ fs: ConfigurationFilesystem,
167
167
  path: str,
168
168
  entry: FilesystemEntry,
169
- parser_items: Optional[Union[dict, ConfigurationParser, str, list]] = None,
169
+ parser_items: dict | ConfigurationParser | str | list | None = None,
170
170
  ) -> None:
171
171
  super().__init__(fs, path, entry)
172
172
  self.parser_items = parser_items
@@ -182,7 +182,7 @@ class ConfigurationEntry(FilesystemEntry):
182
182
 
183
183
  return f"<{self.__class__.__name__} {output}"
184
184
 
185
- def get(self, key, default: Optional[Any] = None) -> Union[ConfigurationEntry, Any, None]:
185
+ def get(self, key, default: Any | None = None) -> ConfigurationEntry | Any | None:
186
186
  """Gets the dictionary key that belongs to this entry using ``key``.
187
187
  Behaves like ``dictionary.get()``.
188
188
 
@@ -197,13 +197,19 @@ class ConfigurationEntry(FilesystemEntry):
197
197
  if not key:
198
198
  raise TypeError("key should be defined")
199
199
 
200
- if key in self.parser_items:
200
+ path = fsutil.join(self.path, key, alt_separator=self.fs.alt_separator)
201
+
202
+ if self.parser_items and key in self.parser_items:
201
203
  return ConfigurationEntry(
202
204
  self.fs,
203
- fsutil.join(self.path, key, alt_separator=self.fs.alt_separator),
205
+ path,
204
206
  self.entry,
205
207
  self.parser_items[key],
206
208
  )
209
+
210
+ if self.entry.is_dir():
211
+ return self.fs.get(path, self.entry)
212
+
207
213
  return default
208
214
 
209
215
  def _write_value_mapping(self, values: dict[str, Any], indentation_nr: int = 0) -> str:
@@ -257,6 +263,11 @@ class ConfigurationEntry(FilesystemEntry):
257
263
  if self.is_file():
258
264
  raise NotADirectoryError()
259
265
 
266
+ if self.parser_items is None and self.entry.is_dir():
267
+ for entry in self.entry.scandir():
268
+ yield ConfigurationEntry(self.fs, entry.name, entry, None)
269
+ return
270
+
260
271
  for key, values in self.parser_items.items():
261
272
  yield ConfigurationEntry(self.fs, key, self.entry, values)
262
273
 
@@ -266,7 +277,7 @@ class ConfigurationEntry(FilesystemEntry):
266
277
  def is_dir(self, follow_symlinks: bool = True) -> bool:
267
278
  """Returns whether this :class:`ConfigurationEntry` can be considered a directory."""
268
279
  # if self.parser_items has keys (thus sub-values), we can consider it a directory.
269
- return hasattr(self.parser_items, "keys")
280
+ return (self.parser_items is None and self.entry.is_dir()) or hasattr(self.parser_items, "keys")
270
281
 
271
282
  def is_symlink(self) -> bool:
272
283
  """Return whether this :class:`ConfigurationEntry` is a symlink or not.
@@ -284,7 +295,7 @@ class ConfigurationEntry(FilesystemEntry):
284
295
  Returns:
285
296
  Whether the ``entry`` and ``key`` exists
286
297
  """
287
- return self.entry.exists() and key in self.parser_items
298
+ return self.parser_items and self.entry.exists() and key in self.parser_items
288
299
 
289
300
  def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result:
290
301
  """Returns the stat from the underlying :class:`.FilesystemEntry` :attr:`entry`."""
@@ -171,7 +171,7 @@ class ConfigurationParser:
171
171
  try:
172
172
  self.parse_file(fh)
173
173
  except Exception as e:
174
- raise ConfigurationParsingError(*e.args) from e
174
+ raise ConfigurationParsingError(e.args) from e
175
175
 
176
176
  if self.collapse_all or self.collapse:
177
177
  self.parsed_data = self._collapse_dict(self.parsed_data)
@@ -8,7 +8,7 @@ import subprocess
8
8
  from configparser import ConfigParser
9
9
  from configparser import Error as ConfigParserError
10
10
  from io import BytesIO
11
- from typing import Any, BinaryIO, Iterator, Optional, TextIO
11
+ from typing import Any, BinaryIO, Iterator, TextIO
12
12
 
13
13
  from defusedxml import ElementTree
14
14
  from dissect.hypervisor.util import vmtar
@@ -73,7 +73,7 @@ class ESXiPlugin(UnixPlugin):
73
73
  if configstore.exists():
74
74
  self._configstore = parse_config_store(configstore.open())
75
75
 
76
- def _cfg(self, path: str) -> Optional[str]:
76
+ def _cfg(self, path: str) -> str | None:
77
77
  if not self._config:
78
78
  self.target.log.warning("No ESXi config!")
79
79
  return None
@@ -87,7 +87,7 @@ class ESXiPlugin(UnixPlugin):
87
87
  return obj.get(value_name) if obj else None
88
88
 
89
89
  @classmethod
90
- def detect(cls, target: Target) -> Optional[Filesystem]:
90
+ def detect(cls, target: Target) -> Filesystem | None:
91
91
  bootbanks = [
92
92
  fs for fs in target.filesystems if fs.path("boot.cfg").exists() and list(fs.path("/").glob("*.v00"))
93
93
  ]
@@ -128,7 +128,7 @@ class ESXiPlugin(UnixPlugin):
128
128
  return "localhost"
129
129
 
130
130
  @export(property=True)
131
- def domain(self) -> Optional[str]:
131
+ def domain(self) -> str | None:
132
132
  if hostname := self._cfg("/adv/Misc/HostName"):
133
133
  return hostname.partition(".")[2]
134
134
 
@@ -146,7 +146,7 @@ class ESXiPlugin(UnixPlugin):
146
146
  return list(result)
147
147
 
148
148
  @export(property=True)
149
- def version(self) -> Optional[str]:
149
+ def version(self) -> str | None:
150
150
  boot_cfg = self.target.fs.path("/bootbank/boot.cfg")
151
151
  if not boot_cfg.exists():
152
152
  return None
@@ -187,11 +187,11 @@ class ESXiPlugin(UnixPlugin):
187
187
  return self._configstore
188
188
 
189
189
  @export(property=True)
190
- def os(self):
190
+ def os(self) -> str:
191
191
  return OperatingSystem.ESXI.value
192
192
 
193
193
 
194
- def _mount_modules(target: Target, sysvol: Filesystem, cfg: dict[str, str]):
194
+ def _mount_modules(target: Target, sysvol: Filesystem, cfg: dict[str, str]) -> None:
195
195
  modules = [m.strip() for m in cfg["modules"].split("---")]
196
196
 
197
197
  for module in modules:
@@ -218,20 +218,22 @@ def _mount_modules(target: Target, sysvol: Filesystem, cfg: dict[str, str]):
218
218
  target.fs.append_layer().mount("/", tfs)
219
219
 
220
220
 
221
- def _mount_local(target: Target, local_layer: VirtualFilesystem):
221
+ def _mount_local(target: Target, local_layer: VirtualFilesystem) -> None:
222
222
  local_tgz = target.fs.path("local.tgz")
223
+ local_tgz_ve = target.fs.path("local.tgz.ve")
223
224
  local_fs = None
224
225
 
225
226
  if local_tgz.exists():
226
227
  local_fs = tar.TarFilesystem(local_tgz.open())
227
- else:
228
- local_tgz_ve = target.fs.path("local.tgz.ve")
228
+ elif local_tgz_ve.exists():
229
229
  # In the case "encryption.info" does not exist, but ".#encryption.info" does
230
230
  encryption_info = next(target.fs.path("/").glob("*encryption.info"), None)
231
231
  if not local_tgz_ve.exists() or not encryption_info.exists():
232
232
  raise ValueError("Unable to find valid configuration archive")
233
233
 
234
234
  local_fs = _create_local_fs(target, local_tgz_ve, encryption_info)
235
+ else:
236
+ target.log.warning("No local.tgz or local.tgz.ve found, skipping local state")
235
237
 
236
238
  if local_fs:
237
239
  local_layer.mount("/", local_fs)
@@ -245,7 +247,7 @@ def _decrypt_envelope(local_tgz_ve: TargetPath, encryption_info: TargetPath) ->
245
247
  return local_tgz
246
248
 
247
249
 
248
- def _decrypt_crypto_util(local_tgz_ve: TargetPath) -> Optional[BytesIO]:
250
+ def _decrypt_crypto_util(local_tgz_ve: TargetPath) -> BytesIO | None:
249
251
  """Decrypt ``local.tgz.ve`` using ESXi ``crypto-util``.
250
252
 
251
253
  We write to stdout, but this results in ``crypto-util`` exiting with a non-zero return code
@@ -264,9 +266,7 @@ def _decrypt_crypto_util(local_tgz_ve: TargetPath) -> Optional[BytesIO]:
264
266
  return BytesIO(result.stdout)
265
267
 
266
268
 
267
- def _create_local_fs(
268
- target: Target, local_tgz_ve: TargetPath, encryption_info: TargetPath
269
- ) -> Optional[tar.TarFilesystem]:
269
+ def _create_local_fs(target: Target, local_tgz_ve: TargetPath, encryption_info: TargetPath) -> tar.TarFilesystem | None:
270
270
  local_tgz = None
271
271
 
272
272
  if HAS_ENVELOPE:
@@ -292,7 +292,7 @@ def _create_local_fs(
292
292
  return tar.TarFilesystem(local_tgz)
293
293
 
294
294
 
295
- def _mount_filesystems(target: Target, sysvol: Filesystem, cfg: dict[str, str]):
295
+ def _mount_filesystems(target: Target, sysvol: Filesystem, cfg: dict[str, str]) -> None:
296
296
  version = cfg["build"]
297
297
 
298
298
  osdata_fs = None
@@ -371,7 +371,7 @@ def _mount_filesystems(target: Target, sysvol: Filesystem, cfg: dict[str, str]):
371
371
  target.fs.symlink(f"/vmfs/volumes/LOCKER-{locker_fs.vmfs.uuid}", "/locker")
372
372
 
373
373
 
374
- def _link_log_dir(target: Target, cfg: dict[str, str], plugin_obj: ESXiPlugin):
374
+ def _link_log_dir(target: Target, cfg: dict[str, str], plugin_obj: ESXiPlugin) -> None:
375
375
  version = cfg["build"]
376
376
 
377
377
  # Don't really know how ESXi does this, but let's just take a shortcut for now
@@ -441,7 +441,7 @@ def parse_esx_conf(fh: TextIO) -> dict[str, Any]:
441
441
  return config
442
442
 
443
443
 
444
- def _traverse(path: str, obj: dict[str, Any], create: bool = False):
444
+ def _traverse(path: str, obj: dict[str, Any], create: bool = False) -> dict[str, Any] | None:
445
445
  parts = path.strip("/").split("/")
446
446
  path_parts = parts[:-1]
447
447
  for part in path_parts:
@@ -62,13 +62,13 @@ class EtcTree(ConfigurationTreePlugin):
62
62
 
63
63
  @export(record=UnixConfigTreeRecord)
64
64
  @arg("--glob", dest="pattern", required=False, default="*", type=str, help="Glob-style pattern to search for")
65
- def etc(self, pattern: str) -> Iterator[UnixConfigTreeRecord]:
66
- for entry, subs, items in self.config_fs.walk("/"):
65
+ @arg("--root", dest="root", required=False, default="/", type=str, help="Path to use as root for search")
66
+ def etc(self, pattern: str, root: str) -> Iterator[UnixConfigTreeRecord]:
67
+ for entry, subs, items in self.config_fs.walk(root):
67
68
  for item in items:
68
69
  try:
69
70
  config_object = self.get(str(Path(entry) / Path(item)))
70
- if isinstance(config_object, ConfigurationEntry):
71
- yield from self._sub(config_object, Path(entry) / Path(item), pattern)
71
+ yield from self._sub(config_object, Path(entry) / Path(item), pattern)
72
72
  except Exception:
73
73
  self.target.log.warning("Could not open configuration item: %s", item)
74
74
  pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.19.dev29
3
+ Version: 3.19.dev31
4
4
  Summary: This module ties all other Dissect modules together, it provides a programming API and command line tools which allow easy access to various data sources inside disk images or file collections (a.k.a. targets)
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License: Affero General Public License v3
@@ -25,7 +25,7 @@ dissect/target/filesystems/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
25
25
  dissect/target/filesystems/ad1.py,sha256=nEPzaaRsb6bL4ItFo0uLdmdLvrmK9BjqHeD3FOp3WQI,2413
26
26
  dissect/target/filesystems/btrfs.py,sha256=5MBi193ZvclkEQcxDr_sDHfj_FYU_hyYNRL4YqpDu4M,6243
27
27
  dissect/target/filesystems/cb.py,sha256=6LcoJiwsYu1Han31IUzVpZVDTifhTLTx_gLfNpB_p6k,5329
28
- dissect/target/filesystems/config.py,sha256=C2JnzBzMqbAjchGFDwURItCeUY7uxkhw1Gen-6cGkAc,11432
28
+ dissect/target/filesystems/config.py,sha256=GQOtixIIt-Jjtpl3IVqUTujcBFfWaAZeAtvxgNUNetU,11963
29
29
  dissect/target/filesystems/cpio.py,sha256=ssVCjkAtLn2FqmNxeo6U5boyUdSYFxLWfXpytHYGPqs,641
30
30
  dissect/target/filesystems/dir.py,sha256=rKEreX3A7CI6a3pMssrO9F-9i5pkxCn_Ucs_dMtHxxA,4574
31
31
  dissect/target/filesystems/exfat.py,sha256=PRkZPUVN5NlgB1VetFtywdNgF6Yj5OBtF5a25t-fFvw,5917
@@ -46,7 +46,7 @@ dissect/target/filesystems/zip.py,sha256=WT1bQhzX_1MXXVZTKrJniae4xqRqMZ8FsfbvhgG
46
46
  dissect/target/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
47
  dissect/target/helpers/cache.py,sha256=TXlJBdFRz6V9zKs903am4Yawr0maYw5kZY0RqklDQJM,8568
48
48
  dissect/target/helpers/config.py,sha256=6917CZ6eDHaK_tOoiVEIndyhRXO6r6eCBIleq6f47PQ,2346
49
- dissect/target/helpers/configutil.py,sha256=SLJFt_k5ezm7CIr_mcZqVrV-2YrcX5D48KgyC9oyWGw,27645
49
+ dissect/target/helpers/configutil.py,sha256=AEnkMQ0e6PncvCqGa-ACzBQWQBhMGBCzO5qzGJtRu60,27644
50
50
  dissect/target/helpers/cyber.py,sha256=WnJlk-HqAETmDAgLq92JPxyDLxvzSoFV_WrO-odVKBI,16805
51
51
  dissect/target/helpers/descriptor_extensions.py,sha256=uT8GwznfDAiIgMM7JKKOY0PXKMv2c0GCqJTCkWFgops,2605
52
52
  dissect/target/helpers/docs.py,sha256=J5U65Y3yOTqxDEZRCdrEmO63XQCeDzOJea1PwPM6Cyc,5146
@@ -208,9 +208,9 @@ dissect/target/plugins/os/unix/bsd/osx/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JC
208
208
  dissect/target/plugins/os/unix/bsd/osx/_os.py,sha256=KvP7YJ7apVwoIop7MR-8q5QbVGoB6MdR42l6ssEe6es,4081
209
209
  dissect/target/plugins/os/unix/bsd/osx/user.py,sha256=qopB0s3n7e6Q7NjWzn8Z-dKtDtU7e6In4Vm7hIvvedo,2322
210
210
  dissect/target/plugins/os/unix/esxi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
211
- dissect/target/plugins/os/unix/esxi/_os.py,sha256=JOJ6j57vFCojgBNkju-7MG2nQqwl4Qc-agXTwjhZPgY,17644
211
+ dissect/target/plugins/os/unix/esxi/_os.py,sha256=s6pAgUyfHh3QcY6sgvk5uVMmLvqK1tIHWR7MSbrFn8w,17789
212
212
  dissect/target/plugins/os/unix/etc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
213
- dissect/target/plugins/os/unix/etc/etc.py,sha256=WNCtO7NWOKRDBiV-XjXqgPuGRDE_Zyw6XWz3kTm__QE,2493
213
+ dissect/target/plugins/os/unix/etc/etc.py,sha256=px_UwtPuk_scD-3nKJQZ0ao5lus9-BrSU4lPZWelYzI,2541
214
214
  dissect/target/plugins/os/unix/linux/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
215
215
  dissect/target/plugins/os/unix/linux/_os.py,sha256=n6VkfGYIdZUxcK2C1aPDUY_ZZQEIl0GkrpvIKeguv5o,2812
216
216
  dissect/target/plugins/os/unix/linux/cmdline.py,sha256=AyMfndt3UsmJtoOyZYC8nWq2GZg9oPvn8SiI3M4NxnE,1622
@@ -346,10 +346,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
346
346
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
347
347
  dissect/target/volumes/md.py,sha256=7ShPtusuLGaIv27SvEETtgsuoQyAa4iAAeOR1NEaajI,1689
348
348
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
349
- dissect.target-3.19.dev29.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
350
- dissect.target-3.19.dev29.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
351
- dissect.target-3.19.dev29.dist-info/METADATA,sha256=mE32WDUFrARni26ZPIFtiWS3iHI51FCZBoIz4IVFVL4,12719
352
- dissect.target-3.19.dev29.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
353
- dissect.target-3.19.dev29.dist-info/entry_points.txt,sha256=BWuxAb_6AvUAQpIQOQU0IMTlaF6TDht2AIZK8bHd-zE,492
354
- dissect.target-3.19.dev29.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
355
- dissect.target-3.19.dev29.dist-info/RECORD,,
349
+ dissect.target-3.19.dev31.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
350
+ dissect.target-3.19.dev31.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
351
+ dissect.target-3.19.dev31.dist-info/METADATA,sha256=GZuNZDNA6Y6mINzf5sqR_gX4uyq0i5TLvFhCse-bxPc,12719
352
+ dissect.target-3.19.dev31.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
353
+ dissect.target-3.19.dev31.dist-info/entry_points.txt,sha256=BWuxAb_6AvUAQpIQOQU0IMTlaF6TDht2AIZK8bHd-zE,492
354
+ dissect.target-3.19.dev31.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
355
+ dissect.target-3.19.dev31.dist-info/RECORD,,