dissect.target 3.17.dev18__py3-none-any.whl → 3.17.dev20__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -525,21 +525,21 @@ class FilesystemEntry:
525
525
  follow_symlinks: Whether to resolve the entry if it is a symbolic link.
526
526
 
527
527
  Returns:
528
- The resolved symbolic link if ``follow_symlinks`` is ``True`` and the ``FilesystemEntry`` is a
529
- symbolic link or else the ``FilesystemEntry`` itself.
528
+ The resolved symbolic link if ``follow_symlinks`` is ``True`` and the :class:`FilesystemEntry` is a
529
+ symbolic link or else the :class:`FilesystemEntry` itself.
530
530
  """
531
531
  if follow_symlinks and self.is_symlink():
532
532
  return self.readlink_ext()
533
533
  return self
534
534
 
535
535
  def get(self, path: str) -> FilesystemEntry:
536
- """Retrieve a FilesystemEntry relative to this entry.
536
+ """Retrieve a :class:`FilesystemEntry` relative to this entry.
537
537
 
538
538
  Args:
539
539
  path: The path relative to this filesystem entry.
540
540
 
541
541
  Returns:
542
- A relative FilesystemEntry.
542
+ A relative :class:`FilesystemEntry`.
543
543
  """
544
544
  raise NotImplementedError()
545
545
 
@@ -560,10 +560,10 @@ class FilesystemEntry:
560
560
  raise NotImplementedError()
561
561
 
562
562
  def scandir(self) -> Iterator[FilesystemEntry]:
563
- """Iterate over the contents of a directory, return them as FilesystemEntry's.
563
+ """Iterate over the contents of a directory, yields :class:`FilesystemEntry`.
564
564
 
565
565
  Returns:
566
- An iterator of directory entries as FilesystemEntry's.
566
+ An iterator of :class:`FilesystemEntry`.
567
567
  """
568
568
  raise NotImplementedError()
569
569
 
@@ -576,10 +576,10 @@ class FilesystemEntry:
576
576
  return list(self.iterdir())
577
577
 
578
578
  def listdir_ext(self) -> list[FilesystemEntry]:
579
- """List the contents of a directory as FilesystemEntry's.
579
+ """List the contents of a directory as a list of :class:`FilesystemEntry`.
580
580
 
581
581
  Returns:
582
- A list of FilesystemEntry's.
582
+ A list of :class:`FilesystemEntry`.
583
583
  """
584
584
  return list(self.scandir())
585
585
 
@@ -614,7 +614,7 @@ class FilesystemEntry:
614
614
  onerror: Optional[Callable] = None,
615
615
  followlinks: bool = False,
616
616
  ) -> Iterator[FilesystemEntry]:
617
- """Walk a directory and show its contents as FilesystemEntry's.
617
+ """Walk a directory and show its contents as :class:`FilesystemEntry`.
618
618
 
619
619
  It walks across all the files inside the entry recursively.
620
620
 
@@ -629,11 +629,11 @@ class FilesystemEntry:
629
629
  followlinks: ``True`` if we want to follow any symbolic link
630
630
 
631
631
  Returns:
632
- An iterator of directory entries as FilesystemEntry's.
632
+ An iterator of :class:`FilesystemEntry`.
633
633
  """
634
634
  yield from fsutil.walk_ext(self, topdown, onerror, followlinks)
635
635
 
636
- def glob(self, pattern) -> Iterator[str]:
636
+ def glob(self, pattern: str) -> Iterator[str]:
637
637
  """Iterate over this directory part of ``patern``, returning entries matching ``pattern`` as strings.
638
638
 
639
639
  Args:
@@ -645,21 +645,23 @@ class FilesystemEntry:
645
645
  for entry in self.glob_ext(pattern):
646
646
  yield entry.path
647
647
 
648
- def glob_ext(self, pattern) -> Iterator[FilesystemEntry]:
649
- """Iterate over the directory part of ``pattern``, returning entries matching ``pattern`` as FilesysmteEntry's.
648
+ def glob_ext(self, pattern: str) -> Iterator[FilesystemEntry]:
649
+ """Iterate over the directory part of ``pattern``, returning entries matching
650
+ ``pattern`` as :class:`FilesysmteEntry`.
650
651
 
651
652
  Args:
652
653
  pattern: The pattern to glob for.
653
654
 
654
655
  Returns:
655
- An iterator of FilesystemEntry's that match the pattern.
656
+ An iterator of :class:`FilesystemEntry` that match the pattern.
656
657
  """
657
658
  yield from fsutil.glob_ext(self, pattern)
658
659
 
659
660
  def exists(self, path: str) -> bool:
660
661
  """Determines whether a ``path``, relative to this entry, exists.
661
662
 
662
- If the `path` is a symbolic link, it will attempt to resolve it to find the FilesystemEntry it points to.
663
+ If the `path` is a symbolic link, it will attempt to resolve it to find
664
+ the :class:`FilesystemEntry` it points to.
663
665
 
664
666
  Args:
665
667
  path: The path relative to this entry.
@@ -737,7 +739,7 @@ class FilesystemEntry:
737
739
  raise NotImplementedError()
738
740
 
739
741
  def readlink_ext(self) -> FilesystemEntry:
740
- """Read the link where this entry points to, return the resulting path as FilesystemEntry.
742
+ """Read the link where this entry points to, return the resulting path as :class:`FilesystemEntry`.
741
743
 
742
744
  If it is a symlink and returns the string that corresponds to that path.
743
745
  This means it follows the path a link points to, it tries to do it recursively.
@@ -860,7 +862,7 @@ class VirtualDirectory(FilesystemEntry):
860
862
  raise TypeError(f"lattr is not allowed on VirtualDirectory: {self.path}")
861
863
 
862
864
  def add(self, name: str, entry: FilesystemEntry) -> None:
863
- """Add an entry to this VirtualDirectory."""
865
+ """Add an entry to this :class:`VirtualDirectory`."""
864
866
  if not self.fs.case_sensitive:
865
867
  name = name.lower()
866
868
 
@@ -1214,7 +1216,7 @@ class VirtualFilesystem(Filesystem):
1214
1216
  self.map_file_entry(vfspath, VirtualFile(self, file_path, fh))
1215
1217
 
1216
1218
  def map_file_entry(self, vfspath: str, entry: FilesystemEntry) -> None:
1217
- """Map a FilesystemEntry into the VFS.
1219
+ """Map a :class:`FilesystemEntry` into the VFS.
1218
1220
 
1219
1221
  Any missing subdirectories up to, but not including, the last part of
1220
1222
  ``vfspath`` will be created.
@@ -1271,7 +1273,7 @@ class VirtualFilesystem(Filesystem):
1271
1273
  return self.map_dir_from_tar(vfspath.lstrip("/"), tar_file, map_single_file=True)
1272
1274
 
1273
1275
  def link(self, src: str, dst: str) -> None:
1274
- """Hard link a FilesystemEntry to another location.
1276
+ """Hard link a :class:`FilesystemEntry` to another location.
1275
1277
 
1276
1278
  Args:
1277
1279
  src: The path to the target of the link.
@@ -1291,65 +1293,132 @@ class VirtualFilesystem(Filesystem):
1291
1293
  self.map_file_entry(dst, VirtualSymlink(self, dst, src))
1292
1294
 
1293
1295
 
1294
- class RootFilesystem(Filesystem):
1295
- __type__ = "root"
1296
+ class LayerFilesystem(Filesystem):
1297
+ __type__ = "layer"
1296
1298
 
1297
- def __init__(self, target: Target):
1298
- self.target = target
1299
- self.layers = []
1299
+ def __init__(self, **kwargs):
1300
+ self.layers: list[Filesystem] = []
1300
1301
  self.mounts = {}
1301
1302
  self._alt_separator = "/"
1302
1303
  self._case_sensitive = True
1303
- self._root_entry = RootFilesystemEntry(self, "/", [])
1304
- self.root = self.add_layer()
1305
- super().__init__(None)
1304
+ self._root_entry = LayerFilesystemEntry(self, "/", [])
1305
+ self.root = self.append_layer()
1306
+ super().__init__(None, **kwargs)
1307
+
1308
+ def __getattr__(self, attr: str) -> Any:
1309
+ """Provide "magic" access to filesystem specific attributes from any of the layers.
1310
+
1311
+ For example, on a :class:`LayerFilesystem` ``fs``, you can do ``fs.ntfs`` to access the
1312
+ internal NTFS object if it has an NTFS layer.
1313
+ """
1314
+ for fs in self.layers:
1315
+ try:
1316
+ return getattr(fs, attr)
1317
+ except AttributeError:
1318
+ continue
1319
+ else:
1320
+ return object.__getattribute__(self, attr)
1306
1321
 
1307
1322
  @staticmethod
1308
1323
  def detect(fh: BinaryIO) -> bool:
1309
- raise TypeError("Detect is not allowed on RootFilesystem class")
1324
+ raise TypeError("Detect is not allowed on LayerFilesystem class")
1310
1325
 
1311
- def mount(self, path: str, fs: Filesystem) -> None:
1326
+ def mount(self, path: str, fs: Filesystem, ignore_existing: bool = True) -> None:
1312
1327
  """Mount a filesystem at a given path.
1313
1328
 
1314
1329
  If there's an overlap with an existing mount, creates a new layer.
1330
+
1331
+ Args:
1332
+ path: The path to mount the filesystem at.
1333
+ fs: The filesystem to mount.
1334
+ ignore_existing: Whether to ignore existing mounts and create a new layer. Defaults to ``True``.
1315
1335
  """
1316
1336
  root = self.root
1317
1337
  for mount in self.mounts.keys():
1318
- if path == mount:
1338
+ if ignore_existing and path == mount:
1319
1339
  continue
1320
1340
 
1321
1341
  if path.startswith(mount):
1322
- root = self.add_layer()
1342
+ root = self.append_layer()
1323
1343
  break
1324
1344
 
1325
1345
  root.map_fs(path, fs)
1326
1346
  self.mounts[path] = fs
1327
1347
 
1328
1348
  def link(self, dst: str, src: str) -> None:
1329
- """Hard link a RootFilesystemEntry to another location."""
1330
- dst = fsutil.normalize(dst, alt_separator=self.alt_separator)
1349
+ """Hard link a :class:`FilesystemEntry` to another location."""
1331
1350
  self.root.map_file_entry(dst, self.get(src))
1332
1351
 
1333
1352
  def symlink(self, dst: str, src: str) -> None:
1334
1353
  """Create a symlink to another location."""
1335
1354
  self.root.symlink(dst, src)
1336
1355
 
1337
- def add_layer(self, **kwargs) -> VirtualFilesystem:
1356
+ def append_layer(self, **kwargs) -> VirtualFilesystem:
1357
+ """Append a new layer."""
1358
+ layer = VirtualFilesystem(case_sensitive=self.case_sensitive, alt_separator=self.alt_separator, **kwargs)
1359
+ self.append_fs_layer(layer)
1360
+ return layer
1361
+
1362
+ add_layer = append_layer
1363
+
1364
+ def prepend_layer(self, **kwargs) -> VirtualFilesystem:
1365
+ """Prepend a new layer."""
1338
1366
  layer = VirtualFilesystem(case_sensitive=self.case_sensitive, alt_separator=self.alt_separator, **kwargs)
1339
- self.layers.append(layer)
1340
- self._root_entry.entries.append(layer.root)
1367
+ self.prepend_fs_layer(layer)
1341
1368
  return layer
1342
1369
 
1370
+ def append_fs_layer(self, fs: Filesystem) -> None:
1371
+ """Append a filesystem as a layer.
1372
+
1373
+ Args:
1374
+ fs: The filesystem to append.
1375
+ """
1376
+ # Counterintuitively, we prepend the filesystem to the list of layers
1377
+ # We could reverse the list of layers upon iteration, but that is a hot path
1378
+ self.layers.insert(0, fs)
1379
+ self._root_entry.entries.insert(0, fs.get("/"))
1380
+
1381
+ def prepend_fs_layer(self, fs: Filesystem) -> None:
1382
+ """Prepend a filesystem as a layer.
1383
+
1384
+ Args:
1385
+ fs: The filesystem to prepend.
1386
+ """
1387
+ # Counterintuitively, we append the filesystem to the list of layers
1388
+ # We could reverse the list of layers upon iteration, but that is a hot path
1389
+ self.layers.append(fs)
1390
+ self._root_entry.entries.append(fs.get("/"))
1391
+
1392
+ def remove_fs_layer(self, fs: Filesystem) -> None:
1393
+ """Remove a filesystem layer.
1394
+
1395
+ Args:
1396
+ fs: The filesystem to remove.
1397
+ """
1398
+ self.remove_layer(self.layers.index(fs))
1399
+
1400
+ def remove_layer(self, idx: int) -> None:
1401
+ """Remove a layer by index.
1402
+
1403
+ Args:
1404
+ idx: The index of the layer to remove.
1405
+ """
1406
+ del self.layers[idx]
1407
+ del self._root_entry.entries[idx]
1408
+
1343
1409
  @property
1344
1410
  def case_sensitive(self) -> bool:
1411
+ """Whether the filesystem is case sensitive."""
1345
1412
  return self._case_sensitive
1346
1413
 
1347
1414
  @property
1348
1415
  def alt_separator(self) -> str:
1416
+ """The alternative separator of the filesystem."""
1349
1417
  return self._alt_separator
1350
1418
 
1351
1419
  @case_sensitive.setter
1352
1420
  def case_sensitive(self, value: bool) -> None:
1421
+ """Set the case sensitivity of the filesystem (and all layers)."""
1353
1422
  self._case_sensitive = value
1354
1423
  self.root.case_sensitive = value
1355
1424
  for layer in self.layers:
@@ -1357,14 +1426,14 @@ class RootFilesystem(Filesystem):
1357
1426
 
1358
1427
  @alt_separator.setter
1359
1428
  def alt_separator(self, value: str) -> None:
1429
+ """Set the alternative separator of the filesystem (and all layers)."""
1360
1430
  self._alt_separator = value
1361
1431
  self.root.alt_separator = value
1362
1432
  for layer in self.layers:
1363
1433
  layer.alt_separator = value
1364
1434
 
1365
- def get(self, path: str, relentry: FilesystemEntry = None) -> FilesystemEntry:
1366
- self.target.log.debug("%r::get(%r)", self, path)
1367
-
1435
+ def get(self, path: str, relentry: Optional[LayerFilesystemEntry] = None) -> LayerFilesystemEntry:
1436
+ """Get a :class:`FilesystemEntry` from the filesystem."""
1368
1437
  entry = relentry or self._root_entry
1369
1438
  path = fsutil.normalize(path, alt_separator=self.alt_separator).strip("/")
1370
1439
  full_path = fsutil.join(entry.path, path, alt_separator=self.alt_separator)
@@ -1388,9 +1457,10 @@ class RootFilesystem(Filesystem):
1388
1457
  raise NotASymlinkError(full_path)
1389
1458
  raise FileNotFoundError(full_path)
1390
1459
 
1391
- return RootFilesystemEntry(self, full_path, entries)
1460
+ return LayerFilesystemEntry(self, full_path, entries)
1392
1461
 
1393
1462
  def _get_from_entry(self, path: str, entry: FilesystemEntry) -> FilesystemEntry:
1463
+ """Get a :class:`FilesystemEntry` relative to a specific entry."""
1394
1464
  parts = path.split("/")
1395
1465
 
1396
1466
  for part in parts:
@@ -1405,11 +1475,11 @@ class RootFilesystem(Filesystem):
1405
1475
  class EntryList(list):
1406
1476
  """Wrapper list for filesystem entries.
1407
1477
 
1408
- Expose a getattr on a list of items. Useful in cases where
1409
- there's a virtual filesystem entry as well as a real one.
1478
+ Exposes a ``__getattr__`` on a list of items. Useful to access internal objects from filesystem implementations.
1479
+ For example, access the underlying NTFS object from a list of virtual and NTFS entries.
1410
1480
  """
1411
1481
 
1412
- def __init__(self, value: Any):
1482
+ def __init__(self, value: FilesystemEntry | list[FilesystemEntry]):
1413
1483
  if not isinstance(value, list):
1414
1484
  value = [value]
1415
1485
  super().__init__(value)
@@ -1424,20 +1494,12 @@ class EntryList(list):
1424
1494
  return object.__getattribute__(self, attr)
1425
1495
 
1426
1496
 
1427
- class RootFilesystemEntry(FilesystemEntry):
1497
+ class LayerFilesystemEntry(FilesystemEntry):
1428
1498
  def __init__(self, fs: Filesystem, path: str, entry: FilesystemEntry):
1429
1499
  super().__init__(fs, path, EntryList(entry))
1430
- self.entries = self.entry
1500
+ self.entries: EntryList = self.entry
1431
1501
  self._link = None
1432
1502
 
1433
- def __getattr__(self, attr):
1434
- for entry in self.entries:
1435
- try:
1436
- return getattr(entry, attr)
1437
- except AttributeError:
1438
- continue
1439
- return object.__getattribute__(self, attr)
1440
-
1441
1503
  def _exec(self, func: str, *args, **kwargs) -> Any:
1442
1504
  """Helper method to execute a method over all contained entries."""
1443
1505
  exc = []
@@ -1451,18 +1513,16 @@ class RootFilesystemEntry(FilesystemEntry):
1451
1513
  exceptions = ",".join(exc)
1452
1514
  else:
1453
1515
  exceptions = "No entries"
1516
+
1454
1517
  raise FilesystemError(f"Can't resolve {func} for {self}: {exceptions}")
1455
1518
 
1456
1519
  def get(self, path: str) -> FilesystemEntry:
1457
- self.fs.target.log.debug("%r::get(%r)", self, path)
1458
1520
  return self.fs.get(path, self._resolve())
1459
1521
 
1460
1522
  def open(self) -> BinaryIO:
1461
- self.fs.target.log.debug("%r::open()", self)
1462
1523
  return self._resolve()._exec("open")
1463
1524
 
1464
1525
  def iterdir(self) -> Iterator[str]:
1465
- self.fs.target.log.debug("%r::iterdir()", self)
1466
1526
  yielded = {".", ".."}
1467
1527
  selfentry = self._resolve()
1468
1528
  for fsentry in selfentry.entries:
@@ -1474,8 +1534,7 @@ class RootFilesystemEntry(FilesystemEntry):
1474
1534
  yield entry_name
1475
1535
  yielded.add(name)
1476
1536
 
1477
- def scandir(self) -> Iterator[FilesystemEntry]:
1478
- self.fs.target.log.debug("%r::scandir()", self)
1537
+ def scandir(self) -> Iterator[LayerFilesystemEntry]:
1479
1538
  # Every entry is actually a list of entries from the different
1480
1539
  # overlaying FSes, of which each may implement a different function
1481
1540
  # like .stat() or .open()
@@ -1495,49 +1554,115 @@ class RootFilesystemEntry(FilesystemEntry):
1495
1554
  # overlaying FSes may have different casing of the name.
1496
1555
  entry_name = entries[0].name
1497
1556
  path = fsutil.join(selfentry.path, entry_name, alt_separator=selfentry.fs.alt_separator)
1498
- yield RootFilesystemEntry(selfentry.fs, path, entries)
1557
+ yield LayerFilesystemEntry(selfentry.fs, path, entries)
1499
1558
 
1500
1559
  def is_file(self, follow_symlinks: bool = True) -> bool:
1501
- self.fs.target.log.debug("%r::is_file()", self)
1502
1560
  try:
1503
1561
  return self._resolve(follow_symlinks=follow_symlinks)._exec("is_file", follow_symlinks=follow_symlinks)
1504
1562
  except FileNotFoundError:
1505
1563
  return False
1506
1564
 
1507
1565
  def is_dir(self, follow_symlinks: bool = True) -> bool:
1508
- self.fs.target.log.debug("%r::is_dir()", self)
1509
1566
  try:
1510
1567
  return self._resolve(follow_symlinks=follow_symlinks)._exec("is_dir", follow_symlinks=follow_symlinks)
1511
1568
  except FileNotFoundError:
1512
1569
  return False
1513
1570
 
1514
1571
  def is_symlink(self) -> bool:
1515
- self.fs.target.log.debug("%r::is_symlink()", self)
1516
1572
  return self._exec("is_symlink")
1517
1573
 
1518
1574
  def readlink(self) -> str:
1519
- self.fs.target.log.debug("%r::readlink()", self)
1520
1575
  if not self.is_symlink():
1521
1576
  raise NotASymlinkError(f"Not a link: {self}")
1522
1577
  return self._exec("readlink")
1523
1578
 
1524
1579
  def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result:
1525
- self.fs.target.log.debug("%r::stat()", self)
1526
1580
  return self._resolve(follow_symlinks=follow_symlinks)._exec("stat", follow_symlinks=follow_symlinks)
1527
1581
 
1528
1582
  def lstat(self) -> fsutil.stat_result:
1529
- self.fs.target.log.debug("%r::lstat()", self)
1530
1583
  return self._exec("lstat")
1531
1584
 
1532
1585
  def attr(self) -> Any:
1533
- self.fs.target.log.debug("%r::attr()", self)
1534
1586
  return self._resolve()._exec("attr")
1535
1587
 
1536
1588
  def lattr(self) -> Any:
1537
- self.fs.target.log.debug("%r::lattr()", self)
1538
1589
  return self._exec("lattr")
1539
1590
 
1540
1591
 
1592
+ class RootFilesystem(LayerFilesystem):
1593
+ __type__ = "root"
1594
+
1595
+ def __init__(self, target: Target):
1596
+ self.target = target
1597
+ super().__init__()
1598
+
1599
+ @staticmethod
1600
+ def detect(fh: BinaryIO) -> bool:
1601
+ raise TypeError("Detect is not allowed on RootFilesystem class")
1602
+
1603
+ def get(self, path: str, relentry: Optional[LayerFilesystemEntry] = None) -> RootFilesystemEntry:
1604
+ self.target.log.debug("%r::get(%r)", self, path)
1605
+ entry = super().get(path, relentry)
1606
+ entry.__class__ = RootFilesystemEntry
1607
+ return entry
1608
+
1609
+
1610
+ class RootFilesystemEntry(LayerFilesystemEntry):
1611
+ fs: RootFilesystem
1612
+
1613
+ def get(self, path: str) -> RootFilesystemEntry:
1614
+ self.fs.target.log.debug("%r::get(%r)", self, path)
1615
+ entry = super().get(path)
1616
+ entry.__class__ = RootFilesystemEntry
1617
+ return entry
1618
+
1619
+ def open(self) -> BinaryIO:
1620
+ self.fs.target.log.debug("%r::open()", self)
1621
+ return super().open()
1622
+
1623
+ def iterdir(self) -> Iterator[str]:
1624
+ self.fs.target.log.debug("%r::iterdir()", self)
1625
+ yield from super().iterdir()
1626
+
1627
+ def scandir(self) -> Iterator[RootFilesystemEntry]:
1628
+ self.fs.target.log.debug("%r::scandir()", self)
1629
+ for entry in super().scandir():
1630
+ entry.__class__ = RootFilesystemEntry
1631
+ yield entry
1632
+
1633
+ def is_file(self, follow_symlinks: bool = True) -> bool:
1634
+ self.fs.target.log.debug("%r::is_file()", self)
1635
+ return super().is_file(follow_symlinks=follow_symlinks)
1636
+
1637
+ def is_dir(self, follow_symlinks: bool = True) -> bool:
1638
+ self.fs.target.log.debug("%r::is_dir()", self)
1639
+ return super().is_dir(follow_symlinks=follow_symlinks)
1640
+
1641
+ def is_symlink(self) -> bool:
1642
+ self.fs.target.log.debug("%r::is_symlink()", self)
1643
+ return super().is_symlink()
1644
+
1645
+ def readlink(self) -> str:
1646
+ self.fs.target.log.debug("%r::readlink()", self)
1647
+ return super().readlink()
1648
+
1649
+ def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result:
1650
+ self.fs.target.log.debug("%r::stat()", self)
1651
+ return super().stat(follow_symlinks=follow_symlinks)
1652
+
1653
+ def lstat(self) -> fsutil.stat_result:
1654
+ self.fs.target.log.debug("%r::lstat()", self)
1655
+ return super().lstat()
1656
+
1657
+ def attr(self) -> Any:
1658
+ self.fs.target.log.debug("%r::attr()", self)
1659
+ return super().attr()
1660
+
1661
+ def lattr(self) -> Any:
1662
+ self.fs.target.log.debug("%r::lattr()", self)
1663
+ return super().lattr()
1664
+
1665
+
1541
1666
  def register(module: str, class_name: str, internal: bool = True) -> None:
1542
1667
  """Register a filesystem implementation to use when opening a filesystem.
1543
1668
 
@@ -14,8 +14,8 @@ class VBLoader(Loader):
14
14
  return (mft_exists or c_drive_exists) and config_exists
15
15
 
16
16
  def map(self, target):
17
- ntfs_overlay = target.fs.add_layer()
18
- remap_overlay = target.fs.add_layer()
17
+ remap_overlay = target.fs.append_layer()
18
+ ntfs_overlay = target.fs.append_layer()
19
19
  dfs = DirectoryFilesystem(self.path, case_sensitive=False)
20
20
  target.filesystems.add(dfs)
21
21
 
@@ -3,7 +3,7 @@ from typing import Iterable
3
3
  from dissect.util.ts import from_unix
4
4
 
5
5
  from dissect.target.exceptions import FileNotFoundError, UnsupportedPluginError
6
- from dissect.target.filesystem import RootFilesystemEntry
6
+ from dissect.target.filesystem import LayerFilesystemEntry
7
7
  from dissect.target.helpers.fsutil import TargetPath
8
8
  from dissect.target.helpers.record import TargetRecordDescriptor
9
9
  from dissect.target.plugin import Plugin, export
@@ -50,7 +50,7 @@ def generate_record(target: Target, path: TargetPath) -> FilesystemRecord:
50
50
  stat = path.lstat()
51
51
  btime = from_unix(stat.st_birthtime) if stat.st_birthtime else None
52
52
  entry = path.get()
53
- if isinstance(entry, RootFilesystemEntry):
53
+ if isinstance(entry, LayerFilesystemEntry):
54
54
  fs_types = [sub_entry.fs.__type__ for sub_entry in entry.entries]
55
55
  else:
56
56
  fs_types = [entry.fs.__type__]
@@ -97,7 +97,7 @@ class ESXiPlugin(UnixPlugin):
97
97
 
98
98
  # Create a root layer for the "local state" filesystem
99
99
  # This stores persistent configuration data
100
- local_layer = target.fs.add_layer()
100
+ local_layer = target.fs.append_layer()
101
101
 
102
102
  # Mount all the visor tars in individual filesystem layers
103
103
  _mount_modules(target, sysvol, cfg)
@@ -209,7 +209,7 @@ def _mount_modules(target: Target, sysvol: Filesystem, cfg: dict[str, str]):
209
209
  tfs = tar.TarFilesystem(cfile, tarinfo=vmtar.VisorTarInfo)
210
210
 
211
211
  if tfs:
212
- target.fs.add_layer().mount("/", tfs)
212
+ target.fs.append_layer().mount("/", tfs)
213
213
 
214
214
 
215
215
  def _mount_local(target: Target, local_layer: VirtualFilesystem):
@@ -24,7 +24,7 @@ class VyosPlugin(LinuxPlugin):
24
24
  self._version, rootpath = latest
25
25
 
26
26
  # VyOS does some additional magic with base system files
27
- layer = target.fs.add_layer()
27
+ layer = target.fs.append_layer()
28
28
  layer.map_file_entry("/", target.fs.root.get(f"/boot/{self._version}/{rootpath}"))
29
29
  super().__init__(target)
30
30
 
@@ -113,7 +113,7 @@ class FortiOSPlugin(LinuxPlugin):
113
113
 
114
114
  # FortiGate
115
115
  if (datafs_tar := sysvol.path("/datafs.tar.gz")).exists():
116
- target.fs.add_layer().mount("/data", TarFilesystem(datafs_tar.open("rb")))
116
+ target.fs.append_layer().mount("/data", TarFilesystem(datafs_tar.open("rb")))
117
117
 
118
118
  # Additional FortiGate or FortiManager tars with corrupt XZ streams
119
119
  target.log.warning("Attempting to load XZ files, this can take a while.")
@@ -127,11 +127,11 @@ class FortiOSPlugin(LinuxPlugin):
127
127
  ):
128
128
  if (tar := target.fs.path(path)).exists() or (tar := sysvol.path(path)).exists():
129
129
  fh = xz.repair_checksum(tar.open("rb"))
130
- target.fs.add_layer().mount("/", TarFilesystem(fh))
130
+ target.fs.append_layer().mount("/", TarFilesystem(fh))
131
131
 
132
132
  # FortiAnalyzer and FortiManager
133
133
  if (rootfs_ext_tar := sysvol.path("rootfs-ext.tar.xz")).exists():
134
- target.fs.add_layer().mount("/", TarFilesystem(rootfs_ext_tar.open("rb")))
134
+ target.fs.append_layer().mount("/", TarFilesystem(rootfs_ext_tar.open("rb")))
135
135
 
136
136
  # Filesystem mounts can be discovered in the FortiCare debug report
137
137
  # or using ``fnsysctl ls`` and ``fnsysctl df`` in the cli.
@@ -31,7 +31,7 @@ from dissect.target.exceptions import (
31
31
  RegistryValueNotFoundError,
32
32
  TargetError,
33
33
  )
34
- from dissect.target.filesystem import FilesystemEntry, RootFilesystemEntry
34
+ from dissect.target.filesystem import FilesystemEntry, LayerFilesystemEntry
35
35
  from dissect.target.helpers import cyber, fsutil, regutil
36
36
  from dissect.target.plugin import arg
37
37
  from dissect.target.target import Target
@@ -469,7 +469,7 @@ class TargetCli(TargetCmd):
469
469
  # If we happen to scan an NTFS filesystem see if any of the
470
470
  # entries has an alternative data stream and also list them.
471
471
  entry = file_.get()
472
- if isinstance(entry, RootFilesystemEntry):
472
+ if isinstance(entry, LayerFilesystemEntry):
473
473
  if entry.entries.fs.__type__ == "ntfs":
474
474
  attrs = entry.lattr()
475
475
  for data_stream in attrs.DATA:
@@ -512,34 +512,66 @@ class TargetCli(TargetCmd):
512
512
  @arg("-l", action="store_true")
513
513
  @arg("-a", "--all", action="store_true") # ignored but included for proper argument parsing
514
514
  @arg("-h", "--human-readable", action="store_true")
515
+ @arg("-R", "--recursive", action="store_true", help="recursively list subdirectories encountered")
516
+ @arg("-c", action="store_true", dest="use_ctime", help="show time when file status was last changed")
517
+ @arg("-u", action="store_true", dest="use_atime", help="show time of last access")
515
518
  def cmd_ls(self, args: argparse.Namespace, stdout: TextIO) -> Optional[bool]:
516
519
  """list directory contents"""
517
520
 
518
521
  path = self.resolve_path(args.path)
519
522
 
523
+ if args.use_ctime and args.use_atime:
524
+ print("can't specify -c and -u at the same time")
525
+ return
526
+
520
527
  if not path or not path.exists():
521
528
  return
522
529
 
530
+ self._print_ls(args, path, 0, stdout)
531
+
532
+ def _print_ls(self, args: argparse.Namespace, path: fsutil.TargetPath, depth: int, stdout: TextIO) -> None:
533
+ path = self.resolve_path(path)
534
+ subdirs = []
535
+
523
536
  if path.is_dir():
524
537
  contents = self.scandir(path, color=True)
525
538
  elif path.is_file():
526
539
  contents = [(path, path.name)]
527
540
 
541
+ if depth > 0:
542
+ print(f"\n{str(path)}:", file=stdout)
543
+
528
544
  if not args.l:
529
- print("\n".join([name for _, name in contents]), file=stdout)
545
+ for target_path, name in contents:
546
+ print(name, file=stdout)
547
+ if target_path.is_dir():
548
+ subdirs.append(target_path)
530
549
  else:
531
550
  if len(contents) > 1:
532
551
  print(f"total {len(contents)}", file=stdout)
533
552
  for target_path, name in contents:
534
- self.print_extensive_file_stat(stdout=stdout, target_path=target_path, name=name)
553
+ self.print_extensive_file_stat(args=args, stdout=stdout, target_path=target_path, name=name)
554
+ if target_path.is_dir():
555
+ subdirs.append(target_path)
556
+
557
+ if args.recursive and subdirs:
558
+ for subdir in subdirs:
559
+ self._print_ls(args, subdir, depth + 1, stdout)
535
560
 
536
- def print_extensive_file_stat(self, stdout: TextIO, target_path: fsutil.TargetPath, name: str) -> None:
561
+ def print_extensive_file_stat(
562
+ self, args: argparse.Namespace, stdout: TextIO, target_path: fsutil.TargetPath, name: str
563
+ ) -> None:
537
564
  """Print the file status."""
538
565
  try:
539
566
  entry = target_path.get()
540
567
  stat = entry.lstat()
541
568
  symlink = f" -> {entry.readlink()}" if entry.is_symlink() else ""
542
- utc_time = datetime.datetime.utcfromtimestamp(stat.st_mtime).isoformat()
569
+ show_time = stat.st_mtime
570
+ if args.use_ctime:
571
+ show_time = stat.st_ctime
572
+ elif args.use_atime:
573
+ show_time = stat.st_atime
574
+ utc_time = datetime.datetime.utcfromtimestamp(show_time).isoformat()
543
575
 
544
576
  print(
545
577
  f"{stat_modestr(stat)} {stat.st_uid:4d} {stat.st_gid:4d} {stat.st_size:6d} {utc_time} {name}{symlink}",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.17.dev18
3
+ Version: 3.17.dev20
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
@@ -1,7 +1,7 @@
1
1
  dissect/target/__init__.py,sha256=Oc7ounTgq2hE4nR6YcNabetc7SQA40ldSa35VEdZcQU,63
2
2
  dissect/target/container.py,sha256=0YcwcGmfJjhPXUB6DEcjWEoSuAtTDxMDpoTviMrLsxM,9353
3
3
  dissect/target/exceptions.py,sha256=VVW_Rq_vQinapz-2mbJ3UkxBEZpb2pE_7JlhMukdtrY,2877
4
- dissect/target/filesystem.py,sha256=VD1BA6hLqH_FPWFZ-wliEuCxnFrUK61S9VbGK7CtA5w,55597
4
+ dissect/target/filesystem.py,sha256=1i-lToeTX-HgQXQOYxPXH-90M_eq43W4FFzNDRdpgpk,60094
5
5
  dissect/target/loader.py,sha256=_mrMOzKdpb7nlZJpLENOLuU4Ty92PzJem9GFDuo0PK4,7298
6
6
  dissect/target/plugin.py,sha256=HAN8maaDt-Rlqt8Rr1IW7gXQpzNQZjCVz-i4aSPphSw,48677
7
7
  dissect/target/report.py,sha256=06uiP4MbNI8cWMVrC1SasNS-Yg6ptjVjckwj8Yhe0Js,7958
@@ -102,7 +102,7 @@ dissect/target/loaders/tar.py,sha256=ah5t9tzplJp-fJrvwTTtfqdjXTpZLQIikWglnaTKQHs
102
102
  dissect/target/loaders/target.py,sha256=MU_HUtg58YdhdZu6ga1sYG7fK61Dn7N0TBkWXDCWwyc,798
103
103
  dissect/target/loaders/targetd.py,sha256=sfbn2_j3il2G-rPywAoNT5YPtD5KmKkmBv1zrPDRs6I,8250
104
104
  dissect/target/loaders/utm.py,sha256=e5x5ZI3HeL0STh4S-CaQb68Rnug4SVZR9zlmHaGFj0M,978
105
- dissect/target/loaders/vb.py,sha256=CnQcn7bAkMzIB1y-lWLtPPXdIVsyeDaT6hTZEurjkV4,2072
105
+ dissect/target/loaders/vb.py,sha256=CdimOMeoJEDq8xYDgtldGSiwhR-dY5uxac1L0sYwAEU,2078
106
106
  dissect/target/loaders/vbox.py,sha256=8JD7D8iAY9JRvTHsrosp5ZMsZezuLhZ10Zt8sEL7KBI,732
107
107
  dissect/target/loaders/velociraptor.py,sha256=FNxZgs_ehmgGO_Giw5oNl7cVOWNqI2nEiPWT4GjF2e0,4955
108
108
  dissect/target/loaders/vma.py,sha256=AAY5-s-nz6wgvmcFkptJD7nNXhpkdf6SqEKVOrJaIKs,644
@@ -160,7 +160,7 @@ dissect/target/plugins/filesystem/acquire_handles.py,sha256=-pX_akH5GrYe0HofXOa2
160
160
  dissect/target/plugins/filesystem/acquire_hash.py,sha256=OVxI19-Bl1tdqCiFMscFMLmyoiBOsuAjL-Q8aQpEwl0,1441
161
161
  dissect/target/plugins/filesystem/icat.py,sha256=bOMi04IlljnKwxTWTZJKtK7RxKnabFu3WcXyUwzkE-4,4090
162
162
  dissect/target/plugins/filesystem/resolver.py,sha256=HfyASUFV4F9uD-yFXilFpPTORAsRDvdmTvuYHgOaOWg,4776
163
- dissect/target/plugins/filesystem/walkfs.py,sha256=aCEBmT3uoQdMdSGUshMOsKpcjrzAFg3HzeYW24PJZwk,2296
163
+ dissect/target/plugins/filesystem/walkfs.py,sha256=e8HEZcV5Wiua26FGWL3xgiQ_PIhcNvGI5KCdsAx2Nmo,2298
164
164
  dissect/target/plugins/filesystem/yara.py,sha256=q_pbrQArNaWP4ILRzK7VQhukIw16LhUvntoviHmZ38Q,2241
165
165
  dissect/target/plugins/filesystem/ntfs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
166
166
  dissect/target/plugins/filesystem/ntfs/mft.py,sha256=Za-fsTcKlAlhm9ugJlMdwsJVf2Osrh4PrEGSFuv-Eeo,9564
@@ -205,7 +205,7 @@ dissect/target/plugins/os/unix/bsd/osx/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JC
205
205
  dissect/target/plugins/os/unix/bsd/osx/_os.py,sha256=KvP7YJ7apVwoIop7MR-8q5QbVGoB6MdR42l6ssEe6es,4081
206
206
  dissect/target/plugins/os/unix/bsd/osx/user.py,sha256=qopB0s3n7e6Q7NjWzn8Z-dKtDtU7e6In4Vm7hIvvedo,2322
207
207
  dissect/target/plugins/os/unix/esxi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
208
- dissect/target/plugins/os/unix/esxi/_os.py,sha256=jqw71St-L_BiREai8bw27oFOrLK4_GuEDLUTK5FMGLU,17498
208
+ dissect/target/plugins/os/unix/esxi/_os.py,sha256=8kFFK9986zN8hXmDUWwdQHtbV33nWKerRuisg_xbsoQ,17504
209
209
  dissect/target/plugins/os/unix/linux/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
210
210
  dissect/target/plugins/os/unix/linux/_os.py,sha256=YJYwuq_iAinOrPqTE49Q4DLYMWBeRCly1uTbDvPhp6Q,2796
211
211
  dissect/target/plugins/os/unix/linux/cmdline.py,sha256=XIvaTL42DzeQGhqHN_RTMI5g8hbI2_wjzb7KZ0kPOM0,1591
@@ -224,10 +224,10 @@ dissect/target/plugins/os/unix/linux/debian/_os.py,sha256=GI19ZqcyfZ1mUYg2NCx93H
224
224
  dissect/target/plugins/os/unix/linux/debian/apt.py,sha256=dkTfLrS-MS8wfrXILFLHDoLqBkM_w16KTRQ7ysiZkZY,4316
225
225
  dissect/target/plugins/os/unix/linux/debian/dpkg.py,sha256=DPBLQiHAF7ZS8IorRsGAiBj4HhvwuJbmkMEHOuTZisw,5735
226
226
  dissect/target/plugins/os/unix/linux/debian/vyos/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
227
- dissect/target/plugins/os/unix/linux/debian/vyos/_os.py,sha256=q8qG2FLJhUbpjfwlNCmWAhFdTWMzSWUh7s7H8m4x7Fw,1741
227
+ dissect/target/plugins/os/unix/linux/debian/vyos/_os.py,sha256=TPjcfv1n68RCe3Er4aCVQwQDCZwJT-NLvje3kPjDfhk,1744
228
228
  dissect/target/plugins/os/unix/linux/fortios/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
229
229
  dissect/target/plugins/os/unix/linux/fortios/_keys.py,sha256=jDDHObfsUn9BGoIir9p4J_-rg9rI1rgoOfnL3R3lg4o,123358
230
- dissect/target/plugins/os/unix/linux/fortios/_os.py,sha256=gFFzByku_3qpSrHpnqJv6xIbIe3V4iGXdUxxGD_-EFA,19435
230
+ dissect/target/plugins/os/unix/linux/fortios/_os.py,sha256=lyURdGaIsPmEhRZ6P5DQtwOggOLe9TZuY1cTirLSukA,19444
231
231
  dissect/target/plugins/os/unix/linux/fortios/generic.py,sha256=tT4-lE0Z_DeDIN3zHrQbE8JB3cRJop1_TiEst-Au0bs,1230
232
232
  dissect/target/plugins/os/unix/linux/fortios/locale.py,sha256=VDdk60sqe2JTfftssO05C667-_BpI3kcqKOTVzO3ueU,5209
233
233
  dissect/target/plugins/os/unix/linux/redhat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -322,7 +322,7 @@ dissect/target/tools/logging.py,sha256=5ZnumtMWLyslxfrUGZ4ntRyf3obOOhmn8SBjKfdLc
322
322
  dissect/target/tools/mount.py,sha256=L_0tSmiBdW4aSaF0vXjB0bAkTC0kmT2N1hrbW6s5Jow,3254
323
323
  dissect/target/tools/query.py,sha256=6zz9SXS6YnHj7eguORS8Je7N4iM0i1PZDIQ-gyJ1nPY,15593
324
324
  dissect/target/tools/reg.py,sha256=FDsiBBDxjWVUBTRj8xn82vZe-J_d9piM-TKS3PHZCcM,3193
325
- dissect/target/tools/shell.py,sha256=6GF-mr4EpzX34G9sqTNKcccFJXhNTlrUqriRYIW7P7o,43544
325
+ dissect/target/tools/shell.py,sha256=4v6Z06YJDjKv6e6SRvWNjQ2n_KHo_CjL4P0w1_gY_ro,44827
326
326
  dissect/target/tools/utils.py,sha256=sQizexY3ui5vmWw4KOBLg5ecK3TPFjD-uxDqRn56ZTY,11304
327
327
  dissect/target/tools/dump/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
328
328
  dissect/target/tools/dump/run.py,sha256=aD84peRS4zHqC78fH7Vd4ni3m1ZmVP70LyMwBRvoDGY,9463
@@ -336,10 +336,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
336
336
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
337
337
  dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
338
338
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
339
- dissect.target-3.17.dev18.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
340
- dissect.target-3.17.dev18.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
341
- dissect.target-3.17.dev18.dist-info/METADATA,sha256=hkVxbE5ESL9e5xGrI4YY5UYYKF66WwuUIgRb8XRsaaQ,11300
342
- dissect.target-3.17.dev18.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
343
- dissect.target-3.17.dev18.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
344
- dissect.target-3.17.dev18.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
345
- dissect.target-3.17.dev18.dist-info/RECORD,,
339
+ dissect.target-3.17.dev20.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
340
+ dissect.target-3.17.dev20.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
341
+ dissect.target-3.17.dev20.dist-info/METADATA,sha256=w19ITKpjeWu52HPdEU4SD_axvpgGpBr8_nsETBOLeEw,11300
342
+ dissect.target-3.17.dev20.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
343
+ dissect.target-3.17.dev20.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
344
+ dissect.target-3.17.dev20.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
345
+ dissect.target-3.17.dev20.dist-info/RECORD,,