dissect.target 3.14.dev9__py3-none-any.whl → 3.14.dev10__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -45,10 +45,12 @@ class Filesystem:
45
45
  # This has the added benefit of having a readily available "pretty name" for each implementation
46
46
  __type__: str = None
47
47
  """A short string identifying the type of filesystem."""
48
+ __multi_volume__: bool = False
49
+ """Whether this filesystem supports multiple volumes (disks)."""
48
50
 
49
51
  def __init__(
50
52
  self,
51
- volume: Optional[BinaryIO] = None,
53
+ volume: Optional[Union[BinaryIO, list[BinaryIO]]] = None,
52
54
  alt_separator: str = "",
53
55
  case_sensitive: bool = True,
54
56
  ) -> None:
@@ -56,7 +58,7 @@ class Filesystem:
56
58
 
57
59
  Args:
58
60
  volume: A volume or other file-like object associated with the filesystem.
59
- case_sensitive: Defines if the paths in the Filesystem are case sensitive or not.
61
+ case_sensitive: Defines if the paths in the filesystem are case sensitive or not.
60
62
  alt_separator: The alternative separator used to distingish between directories in a path.
61
63
 
62
64
  Raises:
@@ -82,7 +84,7 @@ class Filesystem:
82
84
  return cls.__type__
83
85
 
84
86
  def path(self, *args) -> fsutil.TargetPath:
85
- """Get a specific path from the filesystem."""
87
+ """Instantiate a new path-like object on this filesystem."""
86
88
  return fsutil.TargetPath(self, *args)
87
89
 
88
90
  @classmethod
@@ -125,6 +127,52 @@ class Filesystem:
125
127
  """
126
128
  raise NotImplementedError()
127
129
 
130
+ @classmethod
131
+ def detect_id(cls, fh: BinaryIO) -> Optional[bytes]:
132
+ """Return a filesystem set identifier.
133
+
134
+ Only used in filesystems that support multiple volumes (disks) to find all volumes
135
+ belonging to a single filesystem.
136
+
137
+ Args:
138
+ fh: A file-like object, usually a disk or partition.
139
+ """
140
+ if not cls.__multi_volume__:
141
+ return None
142
+
143
+ offset = fh.tell()
144
+ try:
145
+ fh.seek(0)
146
+ return cls._detect_id(fh)
147
+ except NotImplementedError:
148
+ raise
149
+ except Exception as e:
150
+ log.warning("Failed to detect ID on %s filesystem", cls.__fstype__)
151
+ log.debug("", exc_info=e)
152
+ finally:
153
+ fh.seek(offset)
154
+
155
+ return None
156
+
157
+ @staticmethod
158
+ def _detect_id(fh: BinaryIO) -> Optional[bytes]:
159
+ """Return a filesystem set identifier.
160
+
161
+ This method should be implemented by subclasses of filesystems that support multiple volumes (disks).
162
+ The position of ``fh`` is guaranteed to be ``0``.
163
+
164
+ Args:
165
+ fh: A file-like object, usually a disk or partition.
166
+
167
+ Returns:
168
+ An identifier that can be used to combine the given ``fh`` with others beloning to the same set.
169
+ """
170
+ raise NotImplementedError()
171
+
172
+ def iter_subfs(self) -> Iterator[Filesystem]:
173
+ """Yield possible sub-filesystems."""
174
+ yield from ()
175
+
128
176
  def get(self, path: str) -> FilesystemEntry:
129
177
  """Retrieve a :class:`FilesystemEntry` from the filesystem.
130
178
 
@@ -1461,6 +1509,18 @@ def register(module: str, class_name: str, internal: bool = True) -> None:
1461
1509
  FILESYSTEMS.append(getattr(import_lazy(module), class_name))
1462
1510
 
1463
1511
 
1512
+ def is_multi_volume_filesystem(fh: BinaryIO) -> bool:
1513
+ for filesystem in FILESYSTEMS:
1514
+ try:
1515
+ if filesystem.__multi_volume__ and filesystem.detect(fh):
1516
+ return True
1517
+ except ImportError as e:
1518
+ log.info("Failed to import %s", filesystem)
1519
+ log.debug("", exc_info=e)
1520
+
1521
+ return False
1522
+
1523
+
1464
1524
  def open(fh: BinaryIO, *args, **kwargs) -> Filesystem:
1465
1525
  offset = fh.tell()
1466
1526
  fh.seek(0)
@@ -1469,10 +1529,7 @@ def open(fh: BinaryIO, *args, **kwargs) -> Filesystem:
1469
1529
  for filesystem in FILESYSTEMS:
1470
1530
  try:
1471
1531
  if filesystem.detect(fh):
1472
- instance = filesystem(fh, *args, **kwargs)
1473
- instance.volume = fh
1474
-
1475
- return instance
1532
+ return filesystem(fh, *args, **kwargs)
1476
1533
  except ImportError as e:
1477
1534
  log.info("Failed to import %s", filesystem)
1478
1535
  log.debug("", exc_info=e)
@@ -1482,12 +1539,35 @@ def open(fh: BinaryIO, *args, **kwargs) -> Filesystem:
1482
1539
  raise FilesystemError(f"Failed to open filesystem for {fh}")
1483
1540
 
1484
1541
 
1542
+ def open_multi_volume(fhs: list[BinaryIO], *args, **kwargs) -> Filesystem:
1543
+ for filesystem in FILESYSTEMS:
1544
+ try:
1545
+ if not filesystem.__multi_volume__:
1546
+ continue
1547
+
1548
+ volumes = defaultdict(list)
1549
+ for fh in fhs:
1550
+ if not filesystem.detect(fh):
1551
+ continue
1552
+
1553
+ identifier = filesystem.detect_id(fh)
1554
+ volumes[identifier].append(fh)
1555
+
1556
+ for vols in volumes.values():
1557
+ yield filesystem(vols, *args, **kwargs)
1558
+
1559
+ except ImportError as e:
1560
+ log.info("Failed to import %s", filesystem)
1561
+ log.debug("", exc_info=e)
1562
+
1563
+
1485
1564
  register("ntfs", "NtfsFilesystem")
1486
1565
  register("extfs", "ExtFilesystem")
1487
1566
  register("xfs", "XfsFilesystem")
1488
1567
  register("fat", "FatFilesystem")
1489
1568
  register("ffs", "FfsFilesystem")
1490
1569
  register("vmfs", "VmfsFilesystem")
1570
+ register("btrfs", "BtrfsFilesystem")
1491
1571
  register("exfat", "ExfatFilesystem")
1492
1572
  register("squashfs", "SquashFSFilesystem")
1493
1573
  register("zip", "ZipFilesystem")
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import BinaryIO, Iterator, Optional, Union
4
+
5
+ import dissect.btrfs as btrfs
6
+ from dissect.btrfs.c_btrfs import c_btrfs
7
+
8
+ from dissect.target.exceptions import (
9
+ FileNotFoundError,
10
+ FilesystemError,
11
+ IsADirectoryError,
12
+ NotADirectoryError,
13
+ NotASymlinkError,
14
+ )
15
+ from dissect.target.filesystem import Filesystem, FilesystemEntry
16
+ from dissect.target.helpers import fsutil
17
+
18
+
19
+ class BtrfsFilesystem(Filesystem):
20
+ __fstype__ = "btrfs"
21
+ __multi_volume__ = True
22
+
23
+ def __init__(self, fh: Union[BinaryIO, list[BinaryIO]], *args, **kwargs):
24
+ super().__init__(fh, *args, **kwargs)
25
+ self.btrfs = btrfs.Btrfs(fh)
26
+ self.subfs = self.open_subvolume()
27
+ self.subvolume = self.subfs.subvolume
28
+
29
+ @staticmethod
30
+ def _detect(fh: BinaryIO) -> bool:
31
+ fh.seek(c_btrfs.BTRFS_SUPER_INFO_OFFSET)
32
+ block = fh.read(4096)
33
+ magic = int.from_bytes(block[64:72], "little")
34
+
35
+ return magic == c_btrfs.BTRFS_MAGIC
36
+
37
+ @staticmethod
38
+ def _detect_id(fh: BinaryIO) -> Optional[bytes]:
39
+ # First field is csum, followed by fsid
40
+ fh.seek(c_btrfs.BTRFS_SUPER_INFO_OFFSET + c_btrfs.BTRFS_CSUM_SIZE)
41
+ return fh.read(c_btrfs.BTRFS_FSID_SIZE)
42
+
43
+ def iter_subfs(self) -> Iterator[BtrfsSubvolumeFilesystem]:
44
+ for subvol in self.btrfs.subvolumes():
45
+ if subvol.objectid == self.subfs.subvolume.objectid:
46
+ # Skip the default volume as it's already opened by the main filesystem
47
+ continue
48
+ yield self.open_subvolume(subvolid=subvol.objectid)
49
+
50
+ def open_subvolume(self, subvol: Optional[str] = None, subvolid: Optional[int] = None) -> BtrfsSubvolumeFilesystem:
51
+ return BtrfsSubvolumeFilesystem(self, subvol, subvolid)
52
+
53
+ def get(self, path: str) -> FilesystemEntry:
54
+ return self.subfs.get(path)
55
+
56
+
57
+ class BtrfsSubvolumeFilesystem(Filesystem):
58
+ __fstype__ = "btrfs"
59
+
60
+ def __init__(self, fs: BtrfsFilesystem, subvol: Optional[str] = None, subvolid: Optional[int] = None):
61
+ super().__init__(fs.volume, alt_separator=fs.alt_separator, case_sensitive=fs.case_sensitive)
62
+ if subvol is not None and subvolid is not None:
63
+ raise ValueError("Only one of subvol or subvolid is allowed")
64
+
65
+ self.fs = fs
66
+ self.btrfs = fs.btrfs
67
+ if subvol:
68
+ self.subvolume = self.btrfs.find_subvolume(subvol)
69
+ elif subvolid:
70
+ self.subvolume = self.btrfs.open_subvolume(subvolid)
71
+ else:
72
+ self.subvolume = self.btrfs.default_subvolume
73
+
74
+ def get(self, path: str) -> FilesystemEntry:
75
+ return BtrfsFilesystemEntry(self, path, self._get_node(path))
76
+
77
+ def _get_node(self, path: str, node: Optional[btrfs.INode] = None) -> btrfs.INode:
78
+ try:
79
+ return self.subvolume.get(path, node)
80
+ except btrfs.FileNotFoundError as e:
81
+ raise FileNotFoundError(path, cause=e)
82
+ except btrfs.NotADirectoryError as e:
83
+ raise NotADirectoryError(path, cause=e)
84
+ except btrfs.NotASymlinkError as e:
85
+ raise NotASymlinkError(path, cause=e)
86
+ except btrfs.Error as e:
87
+ raise FileNotFoundError(path, cause=e)
88
+
89
+
90
+ class BtrfsFilesystemEntry(FilesystemEntry):
91
+ fs: BtrfsFilesystem
92
+ entry: btrfs.INode
93
+
94
+ def get(self, path: str) -> FilesystemEntry:
95
+ entry_path = fsutil.join(self.path, path, alt_separator=self.fs.alt_separator)
96
+ entry = self.fs._get_node(path, self.entry)
97
+ return BtrfsFilesystemEntry(self.fs, entry_path, entry)
98
+
99
+ def open(self) -> BinaryIO:
100
+ if self.is_dir():
101
+ raise IsADirectoryError(self.path)
102
+ return self._resolve().entry.open()
103
+
104
+ def _iterdir(self) -> Iterator[btrfs.INode]:
105
+ if not self.is_dir():
106
+ raise NotADirectoryError(self.path)
107
+
108
+ if self.is_symlink():
109
+ for entry in self.readlink_ext().iterdir():
110
+ yield entry
111
+ else:
112
+ for name, entry in self.entry.iterdir():
113
+ if name in (".", ".."):
114
+ continue
115
+
116
+ yield name, entry
117
+
118
+ def iterdir(self) -> Iterator[str]:
119
+ for name, _ in self._iterdir():
120
+ yield name
121
+
122
+ def scandir(self) -> Iterator[FilesystemEntry]:
123
+ for name, entry in self._iterdir():
124
+ entry_path = fsutil.join(self.path, name, alt_separator=self.fs.alt_separator)
125
+ yield BtrfsFilesystemEntry(self.fs, entry_path, entry)
126
+
127
+ def is_dir(self, follow_symlinks: bool = True) -> bool:
128
+ try:
129
+ return self._resolve(follow_symlinks=follow_symlinks).entry.is_dir()
130
+ except FilesystemError:
131
+ return False
132
+
133
+ def is_file(self, follow_symlinks: bool = True) -> bool:
134
+ try:
135
+ return self._resolve(follow_symlinks=follow_symlinks).entry.is_file()
136
+ except FilesystemError:
137
+ return False
138
+
139
+ def is_symlink(self) -> bool:
140
+ return self.entry.is_symlink()
141
+
142
+ def readlink(self) -> str:
143
+ if not self.is_symlink():
144
+ raise NotASymlinkError()
145
+
146
+ return self.entry.link
147
+
148
+ def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result:
149
+ return self._resolve(follow_symlinks=follow_symlinks).lstat()
150
+
151
+ def lstat(self) -> fsutil.stat_result:
152
+ entry = self.entry
153
+ node = self.entry.inode
154
+
155
+ # mode, ino, dev, nlink, uid, gid, size, atime, mtime, ctime
156
+ st_info = st_info = fsutil.stat_result(
157
+ [
158
+ entry.mode,
159
+ entry.inum,
160
+ 0,
161
+ node.nlink,
162
+ entry.uid,
163
+ entry.gid,
164
+ entry.size,
165
+ # timestamp() returns a float which will fill both the integer and float fields
166
+ entry.atime.timestamp(),
167
+ entry.mtime.timestamp(),
168
+ entry.ctime.timestamp(),
169
+ ]
170
+ )
171
+
172
+ # Set the nanosecond resolution separately
173
+ st_info.st_atime_ns = entry.atime_ns
174
+ st_info.st_mtime_ns = entry.mtime_ns
175
+ st_info.st_ctime_ns = entry.ctime_ns
176
+
177
+ # Btrfs has a birth time, called otime
178
+ st_info.st_birthtime = entry.otime.timestamp()
179
+
180
+ return st_info
@@ -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,6 +11,7 @@ from flow.record.fieldtypes import posix_path
11
11
  from dissect.target.filesystem import Filesystem
12
12
  from dissect.target.helpers.fsutil import TargetPath
13
13
  from dissect.target.helpers.record import UnixUserRecord
14
+ from dissect.target.helpers.utils import parse_options_string
14
15
  from dissect.target.plugin import OperatingSystem, OSPlugin, arg, export
15
16
  from dissect.target.target import Target
16
17
 
@@ -177,33 +178,41 @@ class UnixPlugin(OSPlugin):
177
178
  def _add_mounts(self) -> None:
178
179
  fstab = self.target.fs.path("/etc/fstab")
179
180
 
180
- volumes_to_mount = [v for v in self.target.volumes if v.fs]
181
-
182
- for dev_id, volume_name, _, mount_point in parse_fstab(fstab, self.target.log):
183
- for volume in volumes_to_mount:
181
+ for dev_id, volume_name, mount_point, _, options in parse_fstab(fstab, self.target.log):
182
+ opts = parse_options_string(options)
183
+ subvol = opts.get("subvol", None)
184
+ subvolid = opts.get("subvolid", None)
185
+ for fs in self.target.filesystems:
184
186
  fs_id = None
187
+ fs_subvol = None
188
+ fs_subvolid = None
189
+ fs_volume_name = fs.volume.name if fs.volume and not isinstance(fs.volume, list) else None
185
190
  last_mount = None
186
191
 
187
192
  if dev_id:
188
- if volume.fs.__type__ == "xfs":
189
- fs_id = volume.fs.xfs.uuid
190
- elif volume.fs.__type__ == "ext":
191
- fs_id = volume.fs.extfs.uuid
192
- last_mount = volume.fs.extfs.last_mount
193
- elif volume.fs.__type__ == "fat":
194
- fs_id = volume.fs.fatfs.volume_id
193
+ if fs.__type__ == "xfs":
194
+ fs_id = fs.xfs.uuid
195
+ elif fs.__type__ == "ext":
196
+ fs_id = fs.extfs.uuid
197
+ last_mount = fs.extfs.last_mount
198
+ elif fs.__type__ == "btrfs":
199
+ fs_id = fs.btrfs.uuid
200
+ fs_subvol = fs.subvolume.path
201
+ fs_subvolid = fs.subvolume.objectid
202
+ elif fs.__type__ == "fat":
203
+ fs_id = fs.fatfs.volume_id
195
204
  # This normalizes fs_id to comply with libblkid generated UUIDs
196
205
  # This is needed because FAT filesystems don't have a real UUID,
197
206
  # but instead a volume_id which is not case-sensitive
198
207
  fs_id = fs_id[:4].upper() + "-" + fs_id[4:].upper()
199
208
 
200
209
  if (
201
- (fs_id and (fs_id == dev_id))
202
- or (volume.name and (volume.name == volume_name))
210
+ (fs_id and (fs_id == dev_id and (subvol == fs_subvol or subvolid == fs_subvolid)))
211
+ or (fs_volume_name and (fs_volume_name == volume_name))
203
212
  or (last_mount and (last_mount == mount_point))
204
213
  ):
205
- self.target.log.debug("Mounting %s at %s", volume, mount_point)
206
- self.target.fs.mount(mount_point, volume.fs)
214
+ self.target.log.debug("Mounting %s (%s) at %s", fs, fs.volume, mount_point)
215
+ self.target.fs.mount(mount_point, fs)
207
216
 
208
217
  def _parse_os_release(self, glob: Optional[str] = None) -> dict[str, str]:
209
218
  """Parse files containing Unix version information.
@@ -283,7 +292,7 @@ class UnixPlugin(OSPlugin):
283
292
  def parse_fstab(
284
293
  fstab: TargetPath,
285
294
  log: logging.Logger = log,
286
- ) -> Iterator[tuple[Union[uuid.UUID, str], str, str, str]]:
295
+ ) -> Iterator[tuple[Union[uuid.UUID, str], str, str, str, str]]:
287
296
  """Parse fstab file and return a generator that streams the details of entries,
288
297
  with unsupported FS types and block devices filtered away.
289
298
  """
@@ -310,7 +319,7 @@ def parse_fstab(
310
319
  if len(entry_parts) != 6:
311
320
  continue
312
321
 
313
- dev, mount_point, fs_type, _, _, _ = entry_parts
322
+ dev, mount_point, fs_type, options, _, _ = entry_parts
314
323
 
315
324
  if fs_type in SKIP_FS_TYPES:
316
325
  log.warning("Skipped FS type: %s, %s, %s", fs_type, dev, mount_point)
@@ -341,4 +350,4 @@ def parse_fstab(
341
350
  except ValueError:
342
351
  pass
343
352
 
344
- yield dev_id, volume_name, fs_type, mount_point
353
+ yield dev_id, volume_name, mount_point, fs_type, options
dissect/target/target.py CHANGED
@@ -155,6 +155,7 @@ class Target:
155
155
  """Resolve all disks, volumes and filesystems and load an operating system on the current ``Target``."""
156
156
  self.disks.apply()
157
157
  self.volumes.apply()
158
+ self.filesystems.apply()
158
159
  self._init_os()
159
160
  self._applied = True
160
161
 
@@ -712,19 +713,12 @@ class DiskCollection(Collection[container.Container]):
712
713
 
713
714
 
714
715
  class VolumeCollection(Collection[volume.Volume]):
715
- def open(self, vol: volume.Volume) -> None:
716
- try:
717
- if not hasattr(vol, "fs") or vol.fs is None:
718
- vol.fs = filesystem.open(vol)
719
- self.target.log.debug("Opened filesystem: %s on %s", vol.fs, vol)
720
- self.target.filesystems.add(vol.fs)
721
- except FilesystemError as e:
722
- self.target.log.warning("Can't identify filesystem: %s", vol)
723
- self.target.log.debug("", exc_info=e)
724
-
725
716
  def apply(self) -> None:
726
717
  # We don't want later additions to modify the todo, so make a copy
727
718
  todo = self.entries[:]
719
+ fs_volumes = []
720
+ lvm_volumes = []
721
+ encrypted_volumes = []
728
722
 
729
723
  while todo:
730
724
  new_volumes = []
@@ -751,19 +745,19 @@ class VolumeCollection(Collection[volume.Volume]):
751
745
  if vol.offset == 0 and vol.vs and vol.vs.__type__ == "disk":
752
746
  # We are going to re-open a volume system on itself, bail out
753
747
  self.target.log.info("Found volume with offset 0, opening as raw volume instead")
754
- self.open(vol)
748
+ fs_volumes.append(vol)
755
749
  continue
756
750
 
757
751
  try:
758
752
  vs = volume.open(vol)
759
753
  except Exception:
760
754
  # If opening a volume system fails, there's likely none, so open as a filesystem instead
761
- self.open(vol)
755
+ fs_volumes.append(vol)
762
756
  continue
763
757
 
764
758
  if not len(vs.volumes):
765
759
  # We opened an empty volume system, discard
766
- self.open(vol)
760
+ fs_volumes.append(vol)
767
761
  continue
768
762
 
769
763
  self.entries.extend(vs.volumes)
@@ -786,20 +780,30 @@ class VolumeCollection(Collection[volume.Volume]):
786
780
 
787
781
  todo = new_volumes
788
782
 
789
- # ASDF - getting the correct starting system volume
790
- start_fs = None
791
- start_vol = None
792
- for idx, vol in enumerate(self.entries):
793
- if start_fs is None and (vol.name is None):
794
- start_fs = idx
783
+ mv_fs_volumes = []
784
+ for vol in fs_volumes:
785
+ try:
786
+ if getattr(vol, "fs", None) is None:
787
+ if filesystem.is_multi_volume_filesystem(vol):
788
+ mv_fs_volumes.append(vol)
789
+ else:
790
+ vol.fs = filesystem.open(vol)
791
+ self.target.log.debug("Opened filesystem: %s on %s", vol.fs, vol)
795
792
 
796
- if start_vol is None and start_fs is not None and (vol.name is not None and vol.fs is None):
797
- start_vol = idx
793
+ if getattr(vol, "fs", None) is not None:
794
+ self.target.filesystems.add(vol.fs)
795
+ except FilesystemError as e:
796
+ self.target.log.warning("Can't identify filesystem: %s", vol)
797
+ self.target.log.debug("", exc_info=e)
798
798
 
799
- if start_fs is not None and start_vol is not None and (vol.name is not None and vol.fs is None):
800
- rel_vol = idx - start_vol
801
- vol.fs = self.entries[start_fs + rel_vol].fs
799
+ for fs in filesystem.open_multi_volume(mv_fs_volumes):
800
+ self.target.filesystems.add(fs)
801
+ for vol in fs.volume:
802
+ vol.fs = fs
802
803
 
803
804
 
804
805
  class FilesystemCollection(Collection[filesystem.Filesystem]):
805
- pass
806
+ def apply(self) -> None:
807
+ for fs in self.entries:
808
+ for subfs in fs.iter_subfs():
809
+ self.add(subfs)
@@ -3,6 +3,7 @@ import logging
3
3
  from typing import Union
4
4
 
5
5
  from dissect.target import Target, filesystem
6
+ from dissect.target.helpers.utils import parse_options_string
6
7
  from dissect.target.tools.utils import (
7
8
  catch_sigpipe,
8
9
  configure_generic_arguments,
@@ -70,7 +71,7 @@ def main():
70
71
  vfs.mount(fname, fs)
71
72
 
72
73
  # This is kinda silly because fusepy will convert this back into string arguments
73
- options = _parse_options(args.options) if args.options else {}
74
+ options = parse_options_string(args.options) if args.options else {}
74
75
 
75
76
  options["nothreads"] = True
76
77
  options["allow_other"] = True
@@ -83,17 +84,6 @@ def main():
83
84
  parser.exit("FUSE error")
84
85
 
85
86
 
86
- def _parse_options(options: str) -> dict[str, Union[str, bool]]:
87
- result = {}
88
- for opt in options.split(","):
89
- if "=" in opt:
90
- key, _, value = opt.partition("=")
91
- result[key] = value
92
- else:
93
- result[opt] = True
94
- return result
95
-
96
-
97
87
  def _format_options(options: dict[str, Union[str, bool]]) -> str:
98
88
  return ",".join([key if value is True else f"{key}={value}" for key, value in options.items()])
99
89
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.14.dev9
3
+ Version: 3.14.dev10
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
@@ -38,6 +38,7 @@ Requires-Dist: dissect.target[full] ; extra == 'cb'
38
38
  Requires-Dist: carbon-black-cloud-sdk-python ~=1.4.3 ; extra == 'cb'
39
39
  Provides-Extra: full
40
40
  Requires-Dist: asn1crypto ; extra == 'full'
41
+ Requires-Dist: dissect.btrfs <2.0.dev,>=1.0.dev ; extra == 'full'
41
42
  Requires-Dist: dissect.cim <4.0.dev,>=3.0.dev ; extra == 'full'
42
43
  Requires-Dist: dissect.clfs <2.0.dev,>=1.0.dev ; extra == 'full'
43
44
  Requires-Dist: dissect.esedb <4.0.dev,>=3.0.dev ; extra == 'full'
@@ -1,11 +1,11 @@
1
1
  dissect/target/__init__.py,sha256=Oc7ounTgq2hE4nR6YcNabetc7SQA40ldSa35VEdZcQU,63
2
2
  dissect/target/container.py,sha256=9ixufT1_0WhraqttBWwQjG80caToJqvCX8VjFk8d5F0,9307
3
3
  dissect/target/exceptions.py,sha256=VVW_Rq_vQinapz-2mbJ3UkxBEZpb2pE_7JlhMukdtrY,2877
4
- dissect/target/filesystem.py,sha256=iENm42dxq3H-AlI3LN5qFud14P0FcR5ANTGPS5LDhCI,50857
4
+ dissect/target/filesystem.py,sha256=_7hxYD34P5Dxnc7WZZsAHvQcF8KxGUJfGgc1gCyQZA8,53412
5
5
  dissect/target/loader.py,sha256=4ZdX-QJY83NPswTyNG31LUwYXdV1tuByrR2vKKg7d5k,7214
6
6
  dissect/target/plugin.py,sha256=7Gss9pofcWKemwwfeAJ7E6nmJSNnZkBkxTcxUY2wzmk,40526
7
7
  dissect/target/report.py,sha256=06uiP4MbNI8cWMVrC1SasNS-Yg6ptjVjckwj8Yhe0Js,7958
8
- dissect/target/target.py,sha256=tvMIHh-lWhXK_7_PHCs6Bu-tJEopdjDtPR0WyAMayqM,31348
8
+ dissect/target/target.py,sha256=zARjjT1tSMW4opble1KfDIISt3pcxPa50cTwhWC_90c,31431
9
9
  dissect/target/volume.py,sha256=aQZAJiny8jjwkc9UtwIRwy7nINXjCxwpO-_UDfh6-BA,15801
10
10
  dissect/target/containers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  dissect/target/containers/asdf.py,sha256=DJp0QEFwUjy2MFwKYcYqIR_BS1fQT1Yi9Kcmqt0aChM,1366
@@ -22,6 +22,7 @@ dissect/target/containers/vmdk.py,sha256=5fQGkJy4esXONXrKLbhpkQDt8Fwx19YENK2mOm7
22
22
  dissect/target/data/autocompletion/target_bash_completion.sh,sha256=wrOQ_ED-h8WFcjCmY6n4qKl84tWJv9l8ShFHDfJqJyA,3592
23
23
  dissect/target/filesystems/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  dissect/target/filesystems/ad1.py,sha256=nEPzaaRsb6bL4ItFo0uLdmdLvrmK9BjqHeD3FOp3WQI,2413
25
+ dissect/target/filesystems/btrfs.py,sha256=X8HUZJHzArpo04v_A42LM4SQIfQt07TZJJNsYiSvXOE,6247
25
26
  dissect/target/filesystems/cb.py,sha256=kFyZ7oFMkICSEGGFna2vhKnZx9KQKZ2ZSG_ckEWTIBE,5329
26
27
  dissect/target/filesystems/config.py,sha256=Xih61MoEup1rCwQu66SezmTDG4XLOn1hC8ECn_WxX7I,5949
27
28
  dissect/target/filesystems/dir.py,sha256=1RVT0-lomceRLQG5sXHpiyii4Vu7S1MGeQSyT-7BCPY,3851
@@ -56,7 +57,7 @@ dissect/target/helpers/regutil.py,sha256=kX-sSZbW8Qkg29Dn_9zYbaQrwLumrr4Y8zJ1EhH
56
57
  dissect/target/helpers/shell_folder_ids.py,sha256=Behhb8oh0kMxrEk6YYKYigCDZe8Hw5QS6iK_d2hTs2Y,24978
57
58
  dissect/target/helpers/ssh.py,sha256=LPssHXyfL8QYmLi2vpa3wElsGboLG_A1Y8kvOehpUr4,6338
58
59
  dissect/target/helpers/targetd.py,sha256=ELhUulzQ4OgXgHsWhsLgM14vut8Wm6btr7qTynlwKaE,1812
59
- dissect/target/helpers/utils.py,sha256=0v0HkVKzzYvUVMuMWF0rifXS-k9PomJU-DKAZTCiQzA,3636
60
+ dissect/target/helpers/utils.py,sha256=r36Bn0UL0E6Z8ajmQrHzC6RyUxTRdwJ1PNsd904Lmzs,4027
60
61
  dissect/target/helpers/data/windowsZones.xml,sha256=4OijeR7oxI0ZwPTSwCkmtcofOsUCjSnbZ4dQxVOM_4o,50005
61
62
  dissect/target/loaders/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
63
  dissect/target/loaders/ad1.py,sha256=k0bkY0L6NKuQBboua-jM_KgeyIcXAo59NjExDZHMy0o,572
@@ -159,7 +160,7 @@ dissect/target/plugins/general/scrape.py,sha256=Fz7BNXflvuxlnVulyyDhLpyU8D_hJdH6
159
160
  dissect/target/plugins/general/users.py,sha256=IOqopQ9Y7CKGkALRUr16y8DwxsidYC5tcPErGZCXxyA,2845
160
161
  dissect/target/plugins/os/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
161
162
  dissect/target/plugins/os/unix/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
162
- dissect/target/plugins/os/unix/_os.py,sha256=7EMlRzSvziov2DnkTiFj07VzQLmgmop6JeTj0xolIKA,12840
163
+ dissect/target/plugins/os/unix/_os.py,sha256=33xAyHYG5vqpMupoIk_vbwsxAXBBRV1hqwkOOU5Gvg8,13409
163
164
  dissect/target/plugins/os/unix/cronjobs.py,sha256=tVRnUxN0w2IqhOKs68hNeovRUu_ag0W35j9PziPprgQ,3508
164
165
  dissect/target/plugins/os/unix/datetime.py,sha256=gKfBdPyUirt3qmVYfOJ1oZXRPn8wRzssbZxR_ARrtk8,1518
165
166
  dissect/target/plugins/os/unix/etc.py,sha256=HoPEC1hxqurSnAXQAK-jf_HxdBIDe-1z_qSw_n-ViI4,258
@@ -286,7 +287,7 @@ dissect/target/tools/dd.py,sha256=Nlh2CFOCV0ksxyedFp7BuyoQ3tBFi6rK6UO0_k5GR_8,17
286
287
  dissect/target/tools/fs.py,sha256=IL71ntXA_oS92l0NPpqyOVrHOZ-bf3qag1amZzAaeHc,3548
287
288
  dissect/target/tools/info.py,sha256=LcR0WLBo2w0QiOmM0llQfcF0a_hfrqImbJ_QNW1mtbo,5174
288
289
  dissect/target/tools/logging.py,sha256=5ZnumtMWLyslxfrUGZ4ntRyf3obOOhmn8SBjKfdLcEg,4174
289
- dissect/target/tools/mount.py,sha256=jZn5U4CqabxDSu8q8tGggHvZl7eLUateywFJDpcpkAU,2796
290
+ dissect/target/tools/mount.py,sha256=m6Ise8H82jgIW2FN0hXKO4l9t3emKiOi55O4LyEqvxk,2581
290
291
  dissect/target/tools/query.py,sha256=qbQI2kAeFP0_1CxT3UbTIZZ1EZIhotD0rRNXqihZcy4,14926
291
292
  dissect/target/tools/reg.py,sha256=ZB5WDmKfiDvs988kHZVyzF-RIoDLXcXuvdLjuEYvDi4,2181
292
293
  dissect/target/tools/shell.py,sha256=zgabhrXMBFA36g7Rc-8VkF5BL6xc4148vblYnjfI88I,42281
@@ -303,10 +304,10 @@ dissect/target/volumes/luks.py,sha256=v_mHW05KM5iG8JDe47i2V4Q9O0r4rnAMA9m_qc9cYw
303
304
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
304
305
  dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
305
306
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
306
- dissect.target-3.14.dev9.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
307
- dissect.target-3.14.dev9.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
308
- dissect.target-3.14.dev9.dist-info/METADATA,sha256=NZ9cGxlWpXqSrwP6WckOPZhfipHXx-2M9RDEocUvN5U,10975
309
- dissect.target-3.14.dev9.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
310
- dissect.target-3.14.dev9.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
311
- dissect.target-3.14.dev9.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
312
- dissect.target-3.14.dev9.dist-info/RECORD,,
307
+ dissect.target-3.14.dev10.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
308
+ dissect.target-3.14.dev10.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
309
+ dissect.target-3.14.dev10.dist-info/METADATA,sha256=k9I3m8_nSD3iQchZl7cyxC4H3_Hu6EFQAoszbRjdevg,11042
310
+ dissect.target-3.14.dev10.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
311
+ dissect.target-3.14.dev10.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
312
+ dissect.target-3.14.dev10.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
313
+ dissect.target-3.14.dev10.dist-info/RECORD,,