From caef6e773ae4f5a1e0642ef76b198d2cc62e8c24 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Thu, 14 Mar 2024 13:30:53 +0100 Subject: [PATCH 1/7] Create separate LayerFilesystem from RootFilesystem --- dissect/target/filesystem.py | 241 +++++++++++++++----- dissect/target/plugins/filesystem/walkfs.py | 4 +- dissect/target/tools/shell.py | 4 +- tests/test_filesystem.py | 51 ++++- 4 files changed, 226 insertions(+), 74 deletions(-) diff --git a/dissect/target/filesystem.py b/dissect/target/filesystem.py index cd1c89684..719989d90 100644 --- a/dissect/target/filesystem.py +++ b/dissect/target/filesystem.py @@ -525,21 +525,21 @@ def _resolve(self, follow_symlinks: bool = True) -> FilesystemEntry: follow_symlinks: Whether to resolve the entry if it is a symbolic link. Returns: - The resolved symbolic link if ``follow_symlinks`` is ``True`` and the ``FilesystemEntry`` is a - symbolic link or else the ``FilesystemEntry`` itself. + The resolved symbolic link if ``follow_symlinks`` is ``True`` and the :class:`FilesystemEntry` is a + symbolic link or else the :class:`FilesystemEntry` itself. """ if follow_symlinks and self.is_symlink(): return self.readlink_ext() return self def get(self, path: str) -> FilesystemEntry: - """Retrieve a FilesystemEntry relative to this entry. + """Retrieve a :class:`FilesystemEntry` relative to this entry. Args: path: The path relative to this filesystem entry. Returns: - A relative FilesystemEntry. + A relative :class:`FilesystemEntry`. """ raise NotImplementedError() @@ -560,10 +560,10 @@ def iterdir(self) -> Iterator[str]: raise NotImplementedError() def scandir(self) -> Iterator[FilesystemEntry]: - """Iterate over the contents of a directory, return them as FilesystemEntry's. + """Iterate over the contents of a directory, yields :class:`FilesystemEntry`. Returns: - An iterator of directory entries as FilesystemEntry's. + An iterator of :class:`FilesystemEntry`. """ raise NotImplementedError() @@ -576,10 +576,10 @@ def listdir(self) -> list[str]: return list(self.iterdir()) def listdir_ext(self) -> list[FilesystemEntry]: - """List the contents of a directory as FilesystemEntry's. + """List the contents of a directory as a list of :class:`FilesystemEntry`. Returns: - A list of FilesystemEntry's. + A list of :class:`FilesystemEntry`. """ return list(self.scandir()) @@ -614,7 +614,7 @@ def walk_ext( onerror: Optional[Callable] = None, followlinks: bool = False, ) -> Iterator[FilesystemEntry]: - """Walk a directory and show its contents as FilesystemEntry's. + """Walk a directory and show its contents as :class:`FilesystemEntry`. It walks across all the files inside the entry recursively. @@ -629,7 +629,7 @@ def walk_ext( followlinks: ``True`` if we want to follow any symbolic link Returns: - An iterator of directory entries as FilesystemEntry's. + An iterator of :class:`FilesystemEntry`. """ yield from fsutil.walk_ext(self, topdown, onerror, followlinks) @@ -646,20 +646,22 @@ def glob(self, pattern) -> Iterator[str]: yield entry.path def glob_ext(self, pattern) -> Iterator[FilesystemEntry]: - """Iterate over the directory part of ``pattern``, returning entries matching ``pattern`` as FilesysmteEntry's. + """Iterate over the directory part of ``pattern``, returning entries matching + ``pattern`` as :class:`FilesysmteEntry`. Args: pattern: The pattern to glob for. Returns: - An iterator of FilesystemEntry's that match the pattern. + An iterator of :class:`FilesystemEntry` that match the pattern. """ yield from fsutil.glob_ext(self, pattern) def exists(self, path: str) -> bool: """Determines whether a ``path``, relative to this entry, exists. - If the `path` is a symbolic link, it will attempt to resolve it to find the FilesystemEntry it points to. + If the `path` is a symbolic link, it will attempt to resolve it to find + the :class:`FilesystemEntry` it points to. Args: path: The path relative to this entry. @@ -737,7 +739,7 @@ def readlink(self) -> str: raise NotImplementedError() def readlink_ext(self) -> FilesystemEntry: - """Read the link where this entry points to, return the resulting path as FilesystemEntry. + """Read the link where this entry points to, return the resulting path as :class:`FilesystemEntry`. If it is a symlink and returns the string that corresponds to that path. This means it follows the path a link points to, it tries to do it recursively. @@ -860,7 +862,7 @@ def lattr(self) -> Any: raise TypeError(f"lattr is not allowed on VirtualDirectory: {self.path}") def add(self, name: str, entry: FilesystemEntry) -> None: - """Add an entry to this VirtualDirectory.""" + """Add an entry to this :class:`VirtualDirectory`.""" if not self.fs.case_sensitive: name = name.lower() @@ -1214,7 +1216,7 @@ def map_file_fh(self, vfspath: str, fh: BinaryIO) -> None: self.map_file_entry(vfspath, VirtualFile(self, file_path, fh)) def map_file_entry(self, vfspath: str, entry: FilesystemEntry) -> None: - """Map a FilesystemEntry into the VFS. + """Map a :class:`FilesystemEntry` into the VFS. Any missing subdirectories up to, but not including, the last part of ``vfspath`` will be created. @@ -1271,7 +1273,7 @@ def map_file_from_tar(self, vfspath: str, tar_file: str | pathlib.Path) -> None: return self.map_dir_from_tar(vfspath.lstrip("/"), tar_file, map_single_file=True) def link(self, src: str, dst: str) -> None: - """Hard link a FilesystemEntry to another location. + """Hard link a :class:`FilesystemEntry` to another location. Args: src: The path to the target of the link. @@ -1291,31 +1293,49 @@ def symlink(self, src: str, dst: str) -> None: self.map_file_entry(dst, VirtualSymlink(self, dst, src)) -class RootFilesystem(Filesystem): - __type__ = "root" +class LayerFilesystem(Filesystem): + __type__ = "layer" - def __init__(self, target: Target): - self.target = target - self.layers = [] + def __init__(self, **kwargs): + self.layers: list[Filesystem] = [] self.mounts = {} self._alt_separator = "/" self._case_sensitive = True - self._root_entry = RootFilesystemEntry(self, "/", []) + self._root_entry = LayerFilesystemEntry(self, "/", []) self.root = self.add_layer() - super().__init__(None) + super().__init__(None, **kwargs) + + def __getattr__(self, attr: str) -> Any: + """Provide "magic" access to filesystem specific attributes from any of the layers. + + For example, on a :class:`LayerFilesystem` ``fs``, you can do ``fs.ntfs`` to access the + internal NTFS object if it has an NTFS layer. + """ + for fs in self.layers: + try: + return getattr(fs, attr) + except AttributeError: + continue + else: + return object.__getattribute__(self, attr) @staticmethod def detect(fh: BinaryIO) -> bool: - raise TypeError("Detect is not allowed on RootFilesystem class") + raise TypeError("Detect is not allowed on LayerFilesystem class") - def mount(self, path: str, fs: Filesystem) -> None: + def mount(self, path: str, fs: Filesystem, ignore_existing: bool = True) -> None: """Mount a filesystem at a given path. If there's an overlap with an existing mount, creates a new layer. + + Args: + path: The path to mount the filesystem at. + fs: The filesystem to mount. + ignore_existing: Whether to ignore existing mounts and create a new layer. Defaults to ``True``. """ root = self.root for mount in self.mounts.keys(): - if path == mount: + if ignore_existing and path == mount: continue if path.startswith(mount): @@ -1326,8 +1346,7 @@ def mount(self, path: str, fs: Filesystem) -> None: self.mounts[path] = fs def link(self, dst: str, src: str) -> None: - """Hard link a RootFilesystemEntry to another location.""" - dst = fsutil.normalize(dst, alt_separator=self.alt_separator) + """Hard link a :class:`FilesystemEntry` to another location.""" self.root.map_file_entry(dst, self.get(src)) def symlink(self, dst: str, src: str) -> None: @@ -1335,21 +1354,65 @@ def symlink(self, dst: str, src: str) -> None: self.root.symlink(dst, src) def add_layer(self, **kwargs) -> VirtualFilesystem: + """Append a new layer.""" layer = VirtualFilesystem(case_sensitive=self.case_sensitive, alt_separator=self.alt_separator, **kwargs) - self.layers.append(layer) - self._root_entry.entries.append(layer.root) + self.add_fs_layer(layer) return layer + def prepend_layer(self, **kwargs) -> VirtualFilesystem: + """Prepend a new layer.""" + layer = VirtualFilesystem(case_sensitive=self.case_sensitive, alt_separator=self.alt_separator, **kwargs) + self.prepend_fs_layer(layer) + return layer + + def add_fs_layer(self, fs: Filesystem) -> None: + """Append a filesystem as a layer. + + Args: + fs: The filesystem to append. + """ + self.layers.append(fs) + self._root_entry.entries.append(fs.get("/")) + + def prepend_fs_layer(self, fs: Filesystem) -> None: + """Prepend a filesystem as a layer. + + Args: + fs: The filesystem to prepend. + """ + self.layers.insert(0, fs) + self._root_entry.entries.insert(0, fs.get("/")) + + def remove_fs_layer(self, fs: Filesystem) -> None: + """Remove a filesystem layer. + + Args: + fs: The filesystem to remove. + """ + self.remove_layer(self.layers.index(fs)) + + def remove_layer(self, idx: int) -> None: + """Remove a layer by index. + + Args: + idx: The index of the layer to remove. + """ + del self.layers[idx] + del self._root_entry.entries[idx] + @property def case_sensitive(self) -> bool: + """Whether the filesystem is case sensitive.""" return self._case_sensitive @property def alt_separator(self) -> str: + """The alternative separator for the filesystem.""" return self._alt_separator @case_sensitive.setter def case_sensitive(self, value: bool) -> None: + """Set the case sensitivity of the filesystem (and all layers).""" self._case_sensitive = value self.root.case_sensitive = value for layer in self.layers: @@ -1357,14 +1420,14 @@ def case_sensitive(self, value: bool) -> None: @alt_separator.setter def alt_separator(self, value: str) -> None: + """Set the alternative separator for the filesystem (and all layers).""" self._alt_separator = value self.root.alt_separator = value for layer in self.layers: layer.alt_separator = value - def get(self, path: str, relentry: FilesystemEntry = None) -> FilesystemEntry: - self.target.log.debug("%r::get(%r)", self, path) - + def get(self, path: str, relentry: LayerFilesystemEntry = None) -> LayerFilesystemEntry: + """Get a :class:`FilesystemEntry` from the filesystem.""" entry = relentry or self._root_entry path = fsutil.normalize(path, alt_separator=self.alt_separator).strip("/") full_path = fsutil.join(entry.path, path, alt_separator=self.alt_separator) @@ -1388,9 +1451,10 @@ def get(self, path: str, relentry: FilesystemEntry = None) -> FilesystemEntry: raise NotASymlinkError(full_path) raise FileNotFoundError(full_path) - return RootFilesystemEntry(self, full_path, entries) + return LayerFilesystemEntry(self, full_path, entries) - def _get_from_entry(self, path: str, entry: FilesystemEntry) -> FilesystemEntry: + def _get_from_entry(self, path: str, entry: FilesystemEntry) -> LayerFilesystemEntry: + """Get a :class:`FilesystemEntry` relative from a specific entry.""" parts = path.split("/") for part in parts: @@ -1405,11 +1469,11 @@ def _get_from_entry(self, path: str, entry: FilesystemEntry) -> FilesystemEntry: class EntryList(list): """Wrapper list for filesystem entries. - Expose a getattr on a list of items. Useful in cases where - there's a virtual filesystem entry as well as a real one. + Expose a getattr on a list of items. Useful to access internal objects from specific filesystem implementations. + For example, access the underlying NTFS object from a list of virtual and NTFS entries. """ - def __init__(self, value: Any): + def __init__(self, value: FilesystemEntry | list[FilesystemEntry]): if not isinstance(value, list): value = [value] super().__init__(value) @@ -1424,20 +1488,12 @@ def __getattr__(self, attr: str) -> Any: return object.__getattribute__(self, attr) -class RootFilesystemEntry(FilesystemEntry): +class LayerFilesystemEntry(FilesystemEntry): def __init__(self, fs: Filesystem, path: str, entry: FilesystemEntry): super().__init__(fs, path, EntryList(entry)) - self.entries = self.entry + self.entries: EntryList = self.entry self._link = None - def __getattr__(self, attr): - for entry in self.entries: - try: - return getattr(entry, attr) - except AttributeError: - continue - return object.__getattribute__(self, attr) - def _exec(self, func: str, *args, **kwargs) -> Any: """Helper method to execute a method over all contained entries.""" exc = [] @@ -1451,18 +1507,16 @@ def _exec(self, func: str, *args, **kwargs) -> Any: exceptions = ",".join(exc) else: exceptions = "No entries" + raise FilesystemError(f"Can't resolve {func} for {self}: {exceptions}") def get(self, path: str) -> FilesystemEntry: - self.fs.target.log.debug("%r::get(%r)", self, path) return self.fs.get(path, self._resolve()) def open(self) -> BinaryIO: - self.fs.target.log.debug("%r::open()", self) return self._resolve()._exec("open") def iterdir(self) -> Iterator[str]: - self.fs.target.log.debug("%r::iterdir()", self) yielded = {".", ".."} selfentry = self._resolve() for fsentry in selfentry.entries: @@ -1475,7 +1529,6 @@ def iterdir(self) -> Iterator[str]: yielded.add(name) def scandir(self) -> Iterator[FilesystemEntry]: - self.fs.target.log.debug("%r::scandir()", self) # Every entry is actually a list of entries from the different # overlaying FSes, of which each may implement a different function # like .stat() or .open() @@ -1495,49 +1548,113 @@ def scandir(self) -> Iterator[FilesystemEntry]: # overlaying FSes may have different casing of the name. entry_name = entries[0].name path = fsutil.join(selfentry.path, entry_name, alt_separator=selfentry.fs.alt_separator) - yield RootFilesystemEntry(selfentry.fs, path, entries) + yield LayerFilesystemEntry(selfentry.fs, path, entries) def is_file(self, follow_symlinks: bool = True) -> bool: - self.fs.target.log.debug("%r::is_file()", self) try: return self._resolve(follow_symlinks=follow_symlinks)._exec("is_file", follow_symlinks=follow_symlinks) except FileNotFoundError: return False def is_dir(self, follow_symlinks: bool = True) -> bool: - self.fs.target.log.debug("%r::is_dir()", self) try: return self._resolve(follow_symlinks=follow_symlinks)._exec("is_dir", follow_symlinks=follow_symlinks) except FileNotFoundError: return False def is_symlink(self) -> bool: - self.fs.target.log.debug("%r::is_symlink()", self) return self._exec("is_symlink") def readlink(self) -> str: - self.fs.target.log.debug("%r::readlink()", self) if not self.is_symlink(): raise NotASymlinkError(f"Not a link: {self}") return self._exec("readlink") def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result: - self.fs.target.log.debug("%r::stat()", self) return self._resolve(follow_symlinks=follow_symlinks)._exec("stat", follow_symlinks=follow_symlinks) def lstat(self) -> fsutil.stat_result: - self.fs.target.log.debug("%r::lstat()", self) return self._exec("lstat") def attr(self) -> Any: - self.fs.target.log.debug("%r::attr()", self) return self._resolve()._exec("attr") def lattr(self) -> Any: - self.fs.target.log.debug("%r::lattr()", self) return self._exec("lattr") +class RootFilesystem(LayerFilesystem): + __type__ = "root" + + def __init__(self, target: Target): + self.target = target + super().__init__() + + @staticmethod + def detect(fh: BinaryIO) -> bool: + raise TypeError("Detect is not allowed on RootFilesystem class") + + def get(self, path: str, relentry: LayerFilesystemEntry = None) -> LayerFilesystemEntry: + self.target.log.debug("%r::get(%r)", self, path) + entry = super().get(path, relentry) + entry.__class__ = RootFilesystemEntry + return entry + + +class RootFilesystemEntry(LayerFilesystemEntry): + fs: RootFilesystem + + def get(self, path: str) -> RootFilesystemEntry: + self.fs.target.log.debug("%r::get(%r)", self, path) + entry = super().get(path) + entry.__class__ = RootFilesystemEntry + return entry + + def open(self) -> BinaryIO: + self.fs.target.log.debug("%r::open()", self) + return super().open() + + def iterdir(self) -> Iterator[str]: + self.fs.target.log.debug("%r::iterdir()", self) + yield from super().iterdir() + + def scandir(self) -> Iterator[FilesystemEntry]: + self.fs.target.log.debug("%r::scandir()", self) + yield from super().scandir() + + def is_file(self, follow_symlinks: bool = True) -> bool: + self.fs.target.log.debug("%r::is_file()", self) + return super().is_file(follow_symlinks=follow_symlinks) + + def is_dir(self, follow_symlinks: bool = True) -> bool: + self.fs.target.log.debug("%r::is_dir()", self) + return super().is_dir(follow_symlinks=follow_symlinks) + + def is_symlink(self) -> bool: + self.fs.target.log.debug("%r::is_symlink()", self) + return super().is_symlink() + + def readlink(self) -> str: + self.fs.target.log.debug("%r::readlink()", self) + return super().readlink() + + def stat(self, follow_symlinks: bool = True) -> fsutil.stat_result: + self.fs.target.log.debug("%r::stat()", self) + return super().stat(follow_symlinks=follow_symlinks) + + def lstat(self) -> fsutil.stat_result: + self.fs.target.log.debug("%r::lstat()", self) + return super().lstat() + + def attr(self) -> Any: + self.fs.target.log.debug("%r::attr()", self) + return super().attr() + + def lattr(self) -> Any: + self.fs.target.log.debug("%r::lattr()", self) + return super().lattr() + + def register(module: str, class_name: str, internal: bool = True) -> None: """Register a filesystem implementation to use when opening a filesystem. diff --git a/dissect/target/plugins/filesystem/walkfs.py b/dissect/target/plugins/filesystem/walkfs.py index 626eec4df..ba22c0a2d 100644 --- a/dissect/target/plugins/filesystem/walkfs.py +++ b/dissect/target/plugins/filesystem/walkfs.py @@ -3,7 +3,7 @@ from dissect.util.ts import from_unix from dissect.target.exceptions import FileNotFoundError, UnsupportedPluginError -from dissect.target.filesystem import RootFilesystemEntry +from dissect.target.filesystem import LayerFilesystemEntry from dissect.target.helpers.fsutil import TargetPath from dissect.target.helpers.record import TargetRecordDescriptor from dissect.target.plugin import Plugin, export @@ -50,7 +50,7 @@ def generate_record(target: Target, path: TargetPath) -> FilesystemRecord: stat = path.lstat() btime = from_unix(stat.st_birthtime) if stat.st_birthtime else None entry = path.get() - if isinstance(entry, RootFilesystemEntry): + if isinstance(entry, LayerFilesystemEntry): fs_types = [sub_entry.fs.__type__ for sub_entry in entry.entries] else: fs_types = [entry.fs.__type__] diff --git a/dissect/target/tools/shell.py b/dissect/target/tools/shell.py index 77ab76ae2..2f8cb34c3 100644 --- a/dissect/target/tools/shell.py +++ b/dissect/target/tools/shell.py @@ -31,7 +31,7 @@ RegistryValueNotFoundError, TargetError, ) -from dissect.target.filesystem import FilesystemEntry, RootFilesystemEntry +from dissect.target.filesystem import FilesystemEntry, LayerFilesystemEntry from dissect.target.helpers import cyber, fsutil, regutil from dissect.target.plugin import arg from dissect.target.target import Target @@ -469,7 +469,7 @@ def scandir(self, path: str, color: bool = False) -> list[tuple[fsutil.TargetPat # If we happen to scan an NTFS filesystem see if any of the # entries has an alternative data stream and also list them. entry = file_.get() - if isinstance(entry, RootFilesystemEntry): + if isinstance(entry, LayerFilesystemEntry): if entry.entries.fs.__type__ == "ntfs": attrs = entry.lattr() for data_stream in attrs.DATA: diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py index df9764b3b..0d0439342 100644 --- a/tests/test_filesystem.py +++ b/tests/test_filesystem.py @@ -20,6 +20,7 @@ ) from dissect.target.filesystem import ( FilesystemEntry, + LayerFilesystem, MappedFile, NotASymlinkError, RootFilesystem, @@ -797,7 +798,7 @@ def top_virt_dir() -> VirtualDirectory: return VirtualDirectory(Mock(), "") -def test_virutal_directory_stat(virt_dir: VirtualDirectory, top_virt_dir: VirtualDirectory) -> None: +def test_virtual_directory_stat(virt_dir: VirtualDirectory, top_virt_dir: VirtualDirectory) -> None: assert virt_dir.stat(follow_symlinks=False) == virt_dir._stat() assert virt_dir.stat(follow_symlinks=True) == virt_dir._stat() @@ -806,7 +807,7 @@ def test_virutal_directory_stat(virt_dir: VirtualDirectory, top_virt_dir: Virtua assert virt_dir.stat(follow_symlinks=True) == top_virt_dir.stat(follow_symlinks=True) -def test_virutal_directory_lstat(virt_dir: VirtualDirectory, top_virt_dir: VirtualDirectory) -> None: +def test_virtual_directory_lstat(virt_dir: VirtualDirectory, top_virt_dir: VirtualDirectory) -> None: assert virt_dir.lstat() == virt_dir._stat() assert virt_dir.lstat() == virt_dir.stat(follow_symlinks=False) assert virt_dir.lstat().st_mode == stat.S_IFDIR @@ -815,12 +816,12 @@ def test_virutal_directory_lstat(virt_dir: VirtualDirectory, top_virt_dir: Virtu assert virt_dir.lstat() == top_virt_dir.lstat() -def test_virutal_directory_is_dir(virt_dir: VirtualDirectory) -> None: +def test_virtual_directory_is_dir(virt_dir: VirtualDirectory) -> None: assert virt_dir.is_dir(follow_symlinks=True) assert virt_dir.is_dir(follow_symlinks=False) -def test_virutal_directory_is_file(virt_dir: VirtualDirectory) -> None: +def test_virtual_directory_is_file(virt_dir: VirtualDirectory) -> None: assert not virt_dir.is_file(follow_symlinks=True) assert not virt_dir.is_file(follow_symlinks=False) @@ -830,21 +831,21 @@ def virt_file() -> VirtualFile: return VirtualFile(Mock(), "", Mock()) -def test_virutal_file_stat(virt_file: VirtualFile) -> None: +def test_virtual_file_stat(virt_file: VirtualFile) -> None: assert virt_file.stat(follow_symlinks=False) == virt_file.lstat() assert virt_file.stat(follow_symlinks=True) == virt_file.lstat() -def test_virutal_file_lstat(virt_file: VirtualFile) -> None: +def test_virtual_file_lstat(virt_file: VirtualFile) -> None: assert virt_file.lstat().st_mode == stat.S_IFREG -def test_virutal_file_is_dir(virt_file: VirtualFile) -> None: +def test_virtual_file_is_dir(virt_file: VirtualFile) -> None: assert not virt_file.is_dir(follow_symlinks=True) assert not virt_file.is_dir(follow_symlinks=False) -def test_virutal_file_is_file(virt_file: VirtualFile) -> None: +def test_virtual_file_is_file(virt_file: VirtualFile) -> None: assert virt_file.is_file(follow_symlinks=True) assert virt_file.is_file(follow_symlinks=False) @@ -1164,3 +1165,37 @@ def test_virtual_filesystem_map_file_from_tar() -> None: stat = mock_fs.path("/var/example/test.txt").stat() assert ts.from_unix(stat.st_mtime) == datetime(2021, 12, 6, 9, 51, 40, tzinfo=timezone.utc) + + +def test_layer_filesystem() -> None: + lfs = LayerFilesystem() + + vfs1 = VirtualFilesystem() + vfs1.map_file_fh("file1", BytesIO(b"value1")) + + vfs2 = VirtualFilesystem() + vfs2.map_file_fh("file2", BytesIO(b"value2")) + + vfs3 = VirtualFilesystem() + vfs3.map_file_fh("file3", BytesIO(b"value3")) + + vfs4 = VirtualFilesystem() + vfs4.map_file_fh("file1", BytesIO(b"value4")) + + lfs.add_fs_layer(vfs1) + assert lfs.path("file1").read_text() == "value1" + + lfs.add_fs_layer(vfs2) + assert lfs.path("file1").read_text() == "value1" + assert lfs.path("file2").read_text() == "value2" + + lfs.add_fs_layer(vfs3) + assert lfs.path("file1").read_text() == "value1" + assert lfs.path("file2").read_text() == "value2" + assert lfs.path("file3").read_text() == "value3" + + lfs.add_fs_layer(vfs4) + assert lfs.path("file1").read_text() == "value1" + lfs.remove_fs_layer(vfs4) + lfs.prepend_fs_layer(vfs4) + assert lfs.path("file1").read_text() == "value4" From 3ebb99b3ccf1cdb55bb9d2d041b1014d4c384f3d Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Fri, 15 Mar 2024 12:34:08 +0100 Subject: [PATCH 2/7] Reverse precedence of filesystem layers --- dissect/target/filesystem.py | 12 ++++++++---- dissect/target/loaders/vb.py | 2 +- tests/test_filesystem.py | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/dissect/target/filesystem.py b/dissect/target/filesystem.py index 719989d90..3693668f3 100644 --- a/dissect/target/filesystem.py +++ b/dissect/target/filesystem.py @@ -1371,8 +1371,10 @@ def add_fs_layer(self, fs: Filesystem) -> None: Args: fs: The filesystem to append. """ - self.layers.append(fs) - self._root_entry.entries.append(fs.get("/")) + # Counterintuitively, we prepend the filesystem to the list of layers + # We could reverse the list of layers upon iteration, but that is a hot path + self.layers.insert(0, fs) + self._root_entry.entries.insert(0, fs.get("/")) def prepend_fs_layer(self, fs: Filesystem) -> None: """Prepend a filesystem as a layer. @@ -1380,8 +1382,10 @@ def prepend_fs_layer(self, fs: Filesystem) -> None: Args: fs: The filesystem to prepend. """ - self.layers.insert(0, fs) - self._root_entry.entries.insert(0, fs.get("/")) + # Counterintuitively, we append the filesystem to the list of layers + # We could reverse the list of layers upon iteration, but that is a hot path + self.layers.append(fs) + self._root_entry.entries.append(fs.get("/")) def remove_fs_layer(self, fs: Filesystem) -> None: """Remove a filesystem layer. diff --git a/dissect/target/loaders/vb.py b/dissect/target/loaders/vb.py index ecaa7d37b..4a3179e26 100644 --- a/dissect/target/loaders/vb.py +++ b/dissect/target/loaders/vb.py @@ -14,8 +14,8 @@ def detect(path): return (mft_exists or c_drive_exists) and config_exists def map(self, target): - ntfs_overlay = target.fs.add_layer() remap_overlay = target.fs.add_layer() + ntfs_overlay = target.fs.add_layer() dfs = DirectoryFilesystem(self.path, case_sensitive=False) target.filesystems.add(dfs) diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py index 0d0439342..370c54bbc 100644 --- a/tests/test_filesystem.py +++ b/tests/test_filesystem.py @@ -1195,7 +1195,7 @@ def test_layer_filesystem() -> None: assert lfs.path("file3").read_text() == "value3" lfs.add_fs_layer(vfs4) - assert lfs.path("file1").read_text() == "value1" + assert lfs.path("file1").read_text() == "value4" lfs.remove_fs_layer(vfs4) lfs.prepend_fs_layer(vfs4) - assert lfs.path("file1").read_text() == "value4" + assert lfs.path("file1").read_text() == "value1" From 254e93b124b5198d102a0bd76c6191e906414bf6 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Wed, 27 Mar 2024 21:35:43 +0100 Subject: [PATCH 3/7] Address review comments --- dissect/target/filesystem.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/dissect/target/filesystem.py b/dissect/target/filesystem.py index 3693668f3..793d7b36b 100644 --- a/dissect/target/filesystem.py +++ b/dissect/target/filesystem.py @@ -633,7 +633,7 @@ def walk_ext( """ yield from fsutil.walk_ext(self, topdown, onerror, followlinks) - def glob(self, pattern) -> Iterator[str]: + def glob(self, pattern: str) -> Iterator[str]: """Iterate over this directory part of ``patern``, returning entries matching ``pattern`` as strings. Args: @@ -645,7 +645,7 @@ def glob(self, pattern) -> Iterator[str]: for entry in self.glob_ext(pattern): yield entry.path - def glob_ext(self, pattern) -> Iterator[FilesystemEntry]: + def glob_ext(self, pattern: str) -> Iterator[FilesystemEntry]: """Iterate over the directory part of ``pattern``, returning entries matching ``pattern`` as :class:`FilesysmteEntry`. @@ -1353,19 +1353,21 @@ def symlink(self, dst: str, src: str) -> None: """Create a symlink to another location.""" self.root.symlink(dst, src) - def add_layer(self, **kwargs) -> VirtualFilesystem: + def append_layer(self, **kwargs) -> VirtualFilesystem: """Append a new layer.""" layer = VirtualFilesystem(case_sensitive=self.case_sensitive, alt_separator=self.alt_separator, **kwargs) - self.add_fs_layer(layer) + self.append_fs_layer(layer) return layer + add_layer = append_layer + def prepend_layer(self, **kwargs) -> VirtualFilesystem: """Prepend a new layer.""" layer = VirtualFilesystem(case_sensitive=self.case_sensitive, alt_separator=self.alt_separator, **kwargs) self.prepend_fs_layer(layer) return layer - def add_fs_layer(self, fs: Filesystem) -> None: + def append_fs_layer(self, fs: Filesystem) -> None: """Append a filesystem as a layer. Args: @@ -1411,7 +1413,7 @@ def case_sensitive(self) -> bool: @property def alt_separator(self) -> str: - """The alternative separator for the filesystem.""" + """The alternative separator of the filesystem.""" return self._alt_separator @case_sensitive.setter @@ -1424,13 +1426,13 @@ def case_sensitive(self, value: bool) -> None: @alt_separator.setter def alt_separator(self, value: str) -> None: - """Set the alternative separator for the filesystem (and all layers).""" + """Set the alternative separator of the filesystem (and all layers).""" self._alt_separator = value self.root.alt_separator = value for layer in self.layers: layer.alt_separator = value - def get(self, path: str, relentry: LayerFilesystemEntry = None) -> LayerFilesystemEntry: + def get(self, path: str, relentry: Optional[LayerFilesystemEntry] = None) -> LayerFilesystemEntry: """Get a :class:`FilesystemEntry` from the filesystem.""" entry = relentry or self._root_entry path = fsutil.normalize(path, alt_separator=self.alt_separator).strip("/") @@ -1457,8 +1459,8 @@ def get(self, path: str, relentry: LayerFilesystemEntry = None) -> LayerFilesyst return LayerFilesystemEntry(self, full_path, entries) - def _get_from_entry(self, path: str, entry: FilesystemEntry) -> LayerFilesystemEntry: - """Get a :class:`FilesystemEntry` relative from a specific entry.""" + def _get_from_entry(self, path: str, entry: FilesystemEntry) -> FilesystemEntry: + """Get a :class:`FilesystemEntry` relative to a specific entry.""" parts = path.split("/") for part in parts: @@ -1532,7 +1534,7 @@ def iterdir(self) -> Iterator[str]: yield entry_name yielded.add(name) - def scandir(self) -> Iterator[FilesystemEntry]: + def scandir(self) -> Iterator[LayerFilesystemEntry]: # Every entry is actually a list of entries from the different # overlaying FSes, of which each may implement a different function # like .stat() or .open() @@ -1598,7 +1600,7 @@ def __init__(self, target: Target): def detect(fh: BinaryIO) -> bool: raise TypeError("Detect is not allowed on RootFilesystem class") - def get(self, path: str, relentry: LayerFilesystemEntry = None) -> LayerFilesystemEntry: + def get(self, path: str, relentry: LayerFilesystemEntry = None) -> RootFilesystemEntry: self.target.log.debug("%r::get(%r)", self, path) entry = super().get(path, relentry) entry.__class__ = RootFilesystemEntry @@ -1622,9 +1624,11 @@ def iterdir(self) -> Iterator[str]: self.fs.target.log.debug("%r::iterdir()", self) yield from super().iterdir() - def scandir(self) -> Iterator[FilesystemEntry]: + def scandir(self) -> Iterator[RootFilesystemEntry]: self.fs.target.log.debug("%r::scandir()", self) - yield from super().scandir() + for entry in super().scandir(): + entry.__class__ = RootFilesystemEntry + yield entry def is_file(self, follow_symlinks: bool = True) -> bool: self.fs.target.log.debug("%r::is_file()", self) From ce1797789c01cb8a7e00b5de142928f8033096c8 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Wed, 27 Mar 2024 21:38:29 +0100 Subject: [PATCH 4/7] Fix type hint --- dissect/target/filesystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dissect/target/filesystem.py b/dissect/target/filesystem.py index 793d7b36b..ee438e129 100644 --- a/dissect/target/filesystem.py +++ b/dissect/target/filesystem.py @@ -1600,7 +1600,7 @@ def __init__(self, target: Target): def detect(fh: BinaryIO) -> bool: raise TypeError("Detect is not allowed on RootFilesystem class") - def get(self, path: str, relentry: LayerFilesystemEntry = None) -> RootFilesystemEntry: + def get(self, path: str, relentry: Optional[LayerFilesystemEntry] = None) -> RootFilesystemEntry: self.target.log.debug("%r::get(%r)", self, path) entry = super().get(path, relentry) entry.__class__ = RootFilesystemEntry From 543a3913abb51080fab48c75f3b15a4e5b72a7f2 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Wed, 27 Mar 2024 21:55:34 +0100 Subject: [PATCH 5/7] Add test and improve consistency --- dissect/target/filesystem.py | 4 +- dissect/target/loaders/vb.py | 4 +- dissect/target/plugins/os/unix/esxi/_os.py | 4 +- .../plugins/os/unix/linux/debian/vyos/_os.py | 2 +- .../plugins/os/unix/linux/fortios/_os.py | 6 +- tests/_data/plugins/os/windows/catroot/catdb | Bin 131 -> 196608 bytes .../os/windows/catroot/catroot_file_hint.cat | Bin 130 -> 19506 bytes .../windows/catroot/catroot_package_name.cat | Bin 129 -> 9276 bytes .../catroot/catroot_package_name_2.cat | Bin 129 -> 8906 bytes tests/test_filesystem.py | 59 +++++++++++++----- 10 files changed, 52 insertions(+), 27 deletions(-) diff --git a/dissect/target/filesystem.py b/dissect/target/filesystem.py index ee438e129..7562e9ced 100644 --- a/dissect/target/filesystem.py +++ b/dissect/target/filesystem.py @@ -1302,7 +1302,7 @@ def __init__(self, **kwargs): self._alt_separator = "/" self._case_sensitive = True self._root_entry = LayerFilesystemEntry(self, "/", []) - self.root = self.add_layer() + self.root = self.append_layer() super().__init__(None, **kwargs) def __getattr__(self, attr: str) -> Any: @@ -1339,7 +1339,7 @@ def mount(self, path: str, fs: Filesystem, ignore_existing: bool = True) -> None continue if path.startswith(mount): - root = self.add_layer() + root = self.append_layer() break root.map_fs(path, fs) diff --git a/dissect/target/loaders/vb.py b/dissect/target/loaders/vb.py index 4a3179e26..543ef0a46 100644 --- a/dissect/target/loaders/vb.py +++ b/dissect/target/loaders/vb.py @@ -14,8 +14,8 @@ def detect(path): return (mft_exists or c_drive_exists) and config_exists def map(self, target): - remap_overlay = target.fs.add_layer() - ntfs_overlay = target.fs.add_layer() + remap_overlay = target.fs.append_layer() + ntfs_overlay = target.fs.append_layer() dfs = DirectoryFilesystem(self.path, case_sensitive=False) target.filesystems.add(dfs) diff --git a/dissect/target/plugins/os/unix/esxi/_os.py b/dissect/target/plugins/os/unix/esxi/_os.py index d79764244..c46b005ff 100644 --- a/dissect/target/plugins/os/unix/esxi/_os.py +++ b/dissect/target/plugins/os/unix/esxi/_os.py @@ -97,7 +97,7 @@ def create(cls, target: Target, sysvol: Filesystem) -> ESXiPlugin: # Create a root layer for the "local state" filesystem # This stores persistent configuration data - local_layer = target.fs.add_layer() + local_layer = target.fs.append_layer() # Mount all the visor tars in individual filesystem layers _mount_modules(target, sysvol, cfg) @@ -209,7 +209,7 @@ def _mount_modules(target: Target, sysvol: Filesystem, cfg: dict[str, str]): tfs = tar.TarFilesystem(cfile, tarinfo=vmtar.VisorTarInfo) if tfs: - target.fs.add_layer().mount("/", tfs) + target.fs.append_layer().mount("/", tfs) def _mount_local(target: Target, local_layer: VirtualFilesystem): diff --git a/dissect/target/plugins/os/unix/linux/debian/vyos/_os.py b/dissect/target/plugins/os/unix/linux/debian/vyos/_os.py index 745af912c..f090defb7 100644 --- a/dissect/target/plugins/os/unix/linux/debian/vyos/_os.py +++ b/dissect/target/plugins/os/unix/linux/debian/vyos/_os.py @@ -24,7 +24,7 @@ def __init__(self, target: Target): self._version, rootpath = latest # VyOS does some additional magic with base system files - layer = target.fs.add_layer() + layer = target.fs.append_layer() layer.map_file_entry("/", target.fs.root.get(f"/boot/{self._version}/{rootpath}")) super().__init__(target) diff --git a/dissect/target/plugins/os/unix/linux/fortios/_os.py b/dissect/target/plugins/os/unix/linux/fortios/_os.py index 1c8b006ef..026774b0f 100644 --- a/dissect/target/plugins/os/unix/linux/fortios/_os.py +++ b/dissect/target/plugins/os/unix/linux/fortios/_os.py @@ -113,7 +113,7 @@ def create(cls, target: Target, sysvol: Filesystem) -> FortiOSPlugin: # FortiGate if (datafs_tar := sysvol.path("/datafs.tar.gz")).exists(): - target.fs.add_layer().mount("/data", TarFilesystem(datafs_tar.open("rb"))) + target.fs.append_layer().mount("/data", TarFilesystem(datafs_tar.open("rb"))) # Additional FortiGate or FortiManager tars with corrupt XZ streams target.log.warning("Attempting to load XZ files, this can take a while.") @@ -127,11 +127,11 @@ def create(cls, target: Target, sysvol: Filesystem) -> FortiOSPlugin: ): if (tar := target.fs.path(path)).exists() or (tar := sysvol.path(path)).exists(): fh = xz.repair_checksum(tar.open("rb")) - target.fs.add_layer().mount("/", TarFilesystem(fh)) + target.fs.append_layer().mount("/", TarFilesystem(fh)) # FortiAnalyzer and FortiManager if (rootfs_ext_tar := sysvol.path("rootfs-ext.tar.xz")).exists(): - target.fs.add_layer().mount("/", TarFilesystem(rootfs_ext_tar.open("rb"))) + target.fs.append_layer().mount("/", TarFilesystem(rootfs_ext_tar.open("rb"))) # Filesystem mounts can be discovered in the FortiCare debug report # or using ``fnsysctl ls`` and ``fnsysctl df`` in the cli. diff --git a/tests/_data/plugins/os/windows/catroot/catdb b/tests/_data/plugins/os/windows/catroot/catdb index cc3a9b4e0064858f94afb099007853b1ade7e6e2..5c5ec98d6bf56bdb6b00c37c5bcf7bd55b0f3e7b 100644 GIT binary patch literal 196608 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 TfC(@GCcp%k025#WnGpE@X(Te~ literal 131 zcmWN?OA^8$3;@u5Pr(H&Nf>^;4FnKoR5}K`@bvmN@2YPZ^QHP)ryN?}$GSb5?Em{G z?>wJc&dU12ZH}U(C$jDUkBQL+2G@{t3Yp2+7=1JrOyF9X7;7PP5re@@IbpE`-Xk;) Nn~ZN&7@dQ#{Q&*TCn*2` diff --git a/tests/_data/plugins/os/windows/catroot/catroot_file_hint.cat b/tests/_data/plugins/os/windows/catroot/catroot_file_hint.cat index 205dcaee61a96eff6d0e46bd9f0fdbd46f60dd1d..a6237ed3dfa041ab4c3c8e4ad5f38898bdc447e1 100644 GIT binary patch literal 19506 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 bxawj~SLRQq*jGc69FJL3%Jiyx754rM9q3ab literal 129 zcmWN_%MHUI3;@tOQ?Nh-AqLyohHpV?OH@b>oxV9e>0S9dTK~v8=P`Dro^3u}Wh}S# z3`^{9HI5wBwTj-79Mv1^b_*7nz^#ngBmzuC?1J+^NcZU&IYB|K0|eeMdlMl9Z@Ex5 Nn-`<~vSfo!`~i|dCffi2 diff --git a/tests/_data/plugins/os/windows/catroot/catroot_package_name_2.cat b/tests/_data/plugins/os/windows/catroot/catroot_package_name_2.cat index 167dbff7916d5b9a1f581d12c46f6222e78de292..312bf976c00a5472be74ce142a2d39c1e54cecf6 100644 GIT binary patch literal 8906 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 None: vfs2 = VirtualFilesystem() target_dir = vfs2.makedirs("/path/to/target") - layer1 = target_bare.fs.add_layer() + layer1 = target_bare.fs.append_layer() layer1.mount("/", vfs1) - layer2 = target_bare.fs.add_layer() + layer2 = target_bare.fs.append_layer() layer2.mount("/", vfs2) target_entry = target_bare.fs.get("/path/to/symlink/target").readlink_ext() @@ -118,10 +118,10 @@ def test_symlink_files_across_layers(target_bare: Target) -> None: vfs2 = VirtualFilesystem() target_dir = vfs2.makedirs("/path/to/target/derp") - layer1 = target_bare.fs.add_layer() + layer1 = target_bare.fs.append_layer() layer1.mount("/", vfs1) - layer2 = target_bare.fs.add_layer() + layer2 = target_bare.fs.append_layer() layer2.mount("/", vfs2) target_entry = target_bare.fs.get("/path/to/symlink/target/derp") @@ -139,10 +139,10 @@ def test_symlink_to_symlink_across_layers(target_bare: Target) -> None: vfs2 = VirtualFilesystem() vfs2.symlink("../target", "/path/to/target") - layer1 = target_bare.fs.add_layer() + layer1 = target_bare.fs.append_layer() layer1.mount("/", vfs1) - layer2 = target_bare.fs.add_layer() + layer2 = target_bare.fs.append_layer() layer2.mount("/", vfs2) target_entry = target_bare.fs.get("/path/to/symlink/target/").readlink_ext() @@ -158,10 +158,10 @@ def test_recursive_symlink_across_layers(target_bare: Target) -> None: vfs2 = VirtualFilesystem() vfs2.symlink("symlink/target", "/path/to/target") - layer1 = target_bare.fs.add_layer() + layer1 = target_bare.fs.append_layer() layer1.mount("/", vfs1) - layer2 = target_bare.fs.add_layer() + layer2 = target_bare.fs.append_layer() layer2.mount("/", vfs2) with pytest.raises(SymlinkRecursionError): @@ -179,13 +179,13 @@ def test_symlink_across_3_layers(target_bare: Target) -> None: vfs3 = VirtualFilesystem() target_dir = vfs3.makedirs("/path/target") - layer1 = target_bare.fs.add_layer() + layer1 = target_bare.fs.append_layer() layer1.mount("/", vfs1) - layer2 = target_bare.fs.add_layer() + layer2 = target_bare.fs.append_layer() layer2.mount("/", vfs2) - layer3 = target_bare.fs.add_layer() + layer3 = target_bare.fs.append_layer() layer3.mount("/", vfs3) target_entry = target_bare.fs.get("/path/to/symlink/target/").readlink_ext() @@ -204,10 +204,10 @@ def test_recursive_symlink_open_across_layers(target_bare: Target) -> None: vfs2 = VirtualFilesystem() vfs2.symlink("symlink/target", "/path/to/target") - layer1 = target_bare.fs.add_layer() + layer1 = target_bare.fs.append_layer() layer1.mount("/", vfs1) - layer2 = target_bare.fs.add_layer() + layer2 = target_bare.fs.append_layer() layer2.mount("/", vfs2) with pytest.raises(SymlinkRecursionError): @@ -1182,20 +1182,45 @@ def test_layer_filesystem() -> None: vfs4 = VirtualFilesystem() vfs4.map_file_fh("file1", BytesIO(b"value4")) - lfs.add_fs_layer(vfs1) + lfs.append_fs_layer(vfs1) assert lfs.path("file1").read_text() == "value1" - lfs.add_fs_layer(vfs2) + lfs.append_fs_layer(vfs2) assert lfs.path("file1").read_text() == "value1" assert lfs.path("file2").read_text() == "value2" - lfs.add_fs_layer(vfs3) + lfs.append_fs_layer(vfs3) assert lfs.path("file1").read_text() == "value1" assert lfs.path("file2").read_text() == "value2" assert lfs.path("file3").read_text() == "value3" - lfs.add_fs_layer(vfs4) + lfs.append_fs_layer(vfs4) assert lfs.path("file1").read_text() == "value4" lfs.remove_fs_layer(vfs4) lfs.prepend_fs_layer(vfs4) assert lfs.path("file1").read_text() == "value1" + + +def test_layer_filesystem_mount() -> None: + lfs = LayerFilesystem() + + vfs1 = VirtualFilesystem() + vfs1.map_file_fh("file1", BytesIO(b"value1")) + + vfs2 = VirtualFilesystem() + vfs2.map_file_fh("file2", BytesIO(b"value2")) + + lfs.mount("/vfs", vfs1) + lfs.mount("/vfs", vfs2, ignore_existing=True) + + assert lfs.listdir("/vfs") == ["file2"] + + lfs.mount("/vfs", vfs1, ignore_existing=True) + + assert lfs.listdir("/vfs") == ["file1"] + + lfs.mount("/vfs", vfs2, ignore_existing=False) + + assert sorted(lfs.listdir("/vfs")) == ["file1", "file2"] + assert lfs.path("/vfs/file1").read_text() == "value1" + assert lfs.path("/vfs/file2").read_text() == "value2" From afc96549310819e51c5cff9aa9a926b7ca07b83f Mon Sep 17 00:00:00 2001 From: Erik Schamper <1254028+Schamper@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:29:57 +0000 Subject: [PATCH 6/7] Change --- dissect/target/filesystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dissect/target/filesystem.py b/dissect/target/filesystem.py index 7562e9ced..fead09370 100644 --- a/dissect/target/filesystem.py +++ b/dissect/target/filesystem.py @@ -1475,7 +1475,7 @@ def _get_from_entry(self, path: str, entry: FilesystemEntry) -> FilesystemEntry: class EntryList(list): """Wrapper list for filesystem entries. - Expose a getattr on a list of items. Useful to access internal objects from specific filesystem implementations. + Exposes a ``__getattr__`` on a list of items. Useful to access internal objects from specific filesystem implementations. For example, access the underlying NTFS object from a list of virtual and NTFS entries. """ From 90a8330f2987f07fb566108ea9a3e366f533e6b4 Mon Sep 17 00:00:00 2001 From: Erik Schamper <1254028+Schamper@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:44:13 +0000 Subject: [PATCH 7/7] Mand --- dissect/target/filesystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dissect/target/filesystem.py b/dissect/target/filesystem.py index fead09370..b95939556 100644 --- a/dissect/target/filesystem.py +++ b/dissect/target/filesystem.py @@ -1475,7 +1475,7 @@ def _get_from_entry(self, path: str, entry: FilesystemEntry) -> FilesystemEntry: class EntryList(list): """Wrapper list for filesystem entries. - Exposes a ``__getattr__`` on a list of items. Useful to access internal objects from specific filesystem implementations. + Exposes a ``__getattr__`` on a list of items. Useful to access internal objects from filesystem implementations. For example, access the underlying NTFS object from a list of virtual and NTFS entries. """