diff --git a/dissect/target/helpers/config.py b/dissect/target/helpers/config.py index 8909012d1..7a1cc3faa 100644 --- a/dissect/target/helpers/config.py +++ b/dissect/target/helpers/config.py @@ -1,27 +1,35 @@ +from __future__ import annotations + import ast import importlib.machinery import importlib.util import logging from pathlib import Path from types import ModuleType -from typing import Optional, Union log = logging.getLogger(__name__) CONFIG_NAME = ".targetcfg.py" -def load(path: Optional[Union[Path, str]]) -> ModuleType: +def load(paths: list[Path | str] | Path | str | None) -> ModuleType: + """Attempt to load one configuration from the provided path(s).""" + + if isinstance(paths, Path) or isinstance(paths, str): + paths = [paths] + config_spec = importlib.machinery.ModuleSpec("config", None) config = importlib.util.module_from_spec(config_spec) - config_file = _find_config_file(path) + config_file = _find_config_file(paths) + if config_file: config_values = _parse_ast(config_file.read_bytes()) config.__dict__.update(config_values) + return config -def _parse_ast(code: str) -> dict[str, Union[str, int]]: +def _parse_ast(code: str) -> dict[str, str | int]: # Only allow basic value assignments for backwards compatibility obj = {} @@ -49,15 +57,19 @@ def _parse_ast(code: str) -> dict[str, Union[str, int]]: return obj -def _find_config_file(path: Optional[Union[Path, str]]) -> Optional[Path]: - """Find a config file anywhere in the given path and return it. +def _find_config_file(paths: list[Path | str] | None) -> Path | None: + """Find a config file anywhere in the given path(s) and return it. This algorithm allows parts of the path to not exist or the last part to be a filename. It also does not look in the root directory ('/') for config files. """ + if not paths: + return + config_file = None - if path: + + for path in paths: path = Path(path) cur_path = path.absolute() @@ -69,4 +81,7 @@ def _find_config_file(path: Optional[Union[Path, str]]) -> Optional[Path]: config_file = cur_config cur_path = cur_path.parent + if config_file: + break + return config_file diff --git a/dissect/target/target.py b/dissect/target/target.py index ca198baca..1b732a322 100644 --- a/dissect/target/target.py +++ b/dissect/target/target.py @@ -87,9 +87,10 @@ def __init__(self, path: Union[str, Path] = None): self._applied = False try: - self._config = config.load(self.path) + self._config = config.load([self.path, os.getcwd()]) except Exception as e: - self.log.debug("Error loading config file", exc_info=e) + self.log.warning("Error loading config file: %s", self.path) + self.log.debug("", exc_info=e) self._config = config.load(None) # This loads an empty config. # Fill the disks and/or volumes and/or filesystems and apply() will diff --git a/dissect/target/tools/shell.py b/dissect/target/tools/shell.py index 0b46b63f6..29b06781d 100644 --- a/dissect/target/tools/shell.py +++ b/dissect/target/tools/shell.py @@ -58,6 +58,7 @@ except ImportError: # Readline is not available on Windows log.warning("Readline module is not available") + readline = None # ['mode', 'addr', 'dev', 'nlink', 'uid', 'gid', 'size', 'atime', 'mtime', 'ctime'] STAT_TEMPLATE = """ File: {path} {symlink} @@ -111,12 +112,43 @@ class TargetCmd(cmd.Cmd): CMD_PREFIX = "cmd_" + DEFAULT_HISTFILE = "~/.dissect_history" + DEFAULT_HISTFILESIZE = 10_000 + DEFAULT_HISTDIR = None + DEFAULT_HISTDIRFMT = ".dissect_history_{uid}_{target}" + def __init__(self, target: Target): cmd.Cmd.__init__(self) self.target = target self.debug = False self.identchars += "." + self.histfilesize = getattr(target._config, "HISTFILESIZE", self.DEFAULT_HISTFILESIZE) + self.histdir = getattr(target._config, "HISTDIR", self.DEFAULT_HISTDIR) + + if self.histdir: + self.histdirfmt = getattr(target._config, "HISTDIRFMT", self.DEFAULT_HISTDIRFMT) + self.histfile = pathlib.Path(self.histdir).resolve() / pathlib.Path( + self.histdirfmt.format(uid=os.getuid(), target=target.name) + ) + else: + self.histfile = pathlib.Path(getattr(target._config, "HISTFILE", self.DEFAULT_HISTFILE)).expanduser() + + def preloop(self) -> None: + if readline and self.histfile.exists(): + try: + readline.read_history_file(self.histfile) + except Exception as e: + log.debug("Error reading history file: %s", e) + + def postloop(self) -> None: + if readline: + readline.set_history_length(self.histfilesize) + try: + readline.write_history_file(self.histfile) + except Exception as e: + log.debug("Error writing history file: %s", e) + def __getattr__(self, attr: str) -> Any: if attr.startswith("help_"): _, _, command = attr.partition("_") @@ -1241,10 +1273,11 @@ def run_cli(cli: cmd.Cmd) -> None: # Print an empty newline on exit print() return + except KeyboardInterrupt: # Add a line when pressing ctrl+c, so the next one starts at a new line print() - pass + except Exception as e: if cli.debug: log.exception(e) @@ -1252,7 +1285,6 @@ def run_cli(cli: cmd.Cmd) -> None: log.info(e) print(f"*** Unhandled error: {e}") print("If you wish to see the full debug trace, enable debug mode.") - pass @catch_sigpipe