"""Shared functionality for an extraction CLI tool."""

import datetime
import os
import time

import pytz

from dfvfs.analyzer import analyzer as dfvfs_analyzer
from dfvfs.lib import definitions as dfvfs_definitions
from dfvfs.lib import errors as dfvfs_errors
from dfvfs.path import factory as path_spec_factory
from dfvfs.resolver import context as dfvfs_context

# The following import makes sure the analyzers are registered.
from plaso import analyzers  # pylint: disable=unused-import

# The following import makes sure the parsers are registered.
from plaso import parsers  # pylint: disable=unused-import

from plaso.cli import logger
from plaso.cli import status_view
from plaso.cli import storage_media_tool
from plaso.cli import tool_options
from plaso.cli import views
from plaso.cli.helpers import manager as helpers_manager
from plaso.containers import artifacts
from plaso.engine import configurations
from plaso.engine import engine
from plaso.filters import parser_filter
from plaso.helpers import language_tags
from plaso.lib import definitions
from plaso.lib import errors
from plaso.multi_process import extraction_engine as multi_extraction_engine
from plaso.parsers import manager as parsers_manager
from plaso.parsers import presets as parsers_presets
from plaso.single_process import extraction_engine as single_extraction_engine
from plaso.storage import factory as storage_factory


class ExtractionTool(
    storage_media_tool.StorageMediaTool,
    tool_options.HashersOptions,
    tool_options.ProfilingOptions,
    tool_options.StorageFileOptions,
):
    """Extraction CLI tool.

    Attributes:
      list_language_tags (bool): True if the language tags should be listed.
      list_time_zones (bool): True if the time zones should be listed.
    """

    _BYTES_IN_A_MIB = 1024 * 1024

    # Approximately 250 MB of queued items per worker.
    _DEFAULT_QUEUE_SIZE = 125000

    _PRESETS_FILE_NAME = "presets.yaml"

    _SOURCE_TYPES_TO_PREPROCESS = frozenset(
        [
            dfvfs_definitions.SOURCE_TYPE_DIRECTORY,
            dfvfs_definitions.SOURCE_TYPE_STORAGE_MEDIA_DEVICE,
            dfvfs_definitions.SOURCE_TYPE_STORAGE_MEDIA_IMAGE,
        ]
    )

    _SUPPORTED_ARCHIVE_TYPES = {
        "iso9660": "ISO-9660 disk image (.iso) file",
        "modi": "MacOS disk image (.dmg) file",
        "tar": "tape archive (.tar) file",
        "vhdi": "Virtual Hard Disk image (.vhd, .vhdx) file",
        "zip": "ZIP archive (.zip) file",
    }

    def __init__(self, input_reader=None, output_writer=None):
        """Initializes an CLI tool.

        Args:
          input_reader (Optional[InputReader]): input reader, where None indicates
              that the stdin input reader should be used.
          output_writer (Optional[OutputWriter]): output writer, where None
              indicates that the stdout output writer should be used.
        """
        super().__init__(input_reader=input_reader, output_writer=output_writer)
        self._archive_types_string = "none"
        self._artifacts_registry = None
        self._buffer_size = 0
        self._command_line_arguments = None
        self._enable_sigsegv_handler = False
        self._expanded_parser_filter_expression = None
        self._extract_winevt_resources = True
        self._extract_winreg_binary = True
        self._number_of_extraction_workers = 0
        self._parser_filter_expression = None
        self._preferred_codepage = None
        self._preferred_language = None
        self._preferred_time_zone = None
        self._preferred_year = None
        self._presets_file = None
        self._presets_manager = parsers_presets.ParserPresetsManager()
        self._process_compressed_streams = True
        self._process_memory_limit = None
        self._queue_size = self._DEFAULT_QUEUE_SIZE
        self._resolver_context = dfvfs_context.Context()
        self._single_process_mode = False
        self._status_view = status_view.StatusView(self._output_writer, self.NAME)
        self._status_view_file = "status.info"
        self._status_view_interval = 0.5
        self._status_view_mode = status_view.StatusView.MODE_WINDOW
        self._storage_file_path = None
        self._storage_format = definitions.STORAGE_FORMAT_SQLITE
        self._task_storage_format = definitions.STORAGE_FORMAT_SQLITE
        self._temporary_directory = None
        self._worker_memory_limit = None
        self._worker_timeout = None
        self._yara_rules_string = None

        self.list_language_tags = False
        self.list_time_zones = False

    def _CheckStorageFile(self, storage_file_path, warn_about_existing=False):
        """Checks if the storage file path is valid.

        Args:
          storage_file_path (str): path of the storage file.
          warn_about_existing (bool): True if the user should be warned about
              the storage file already existing.

        Raises:
          BadConfigOption: if the storage file path is invalid.
        """
        if os.path.exists(storage_file_path):
            if not os.path.isfile(storage_file_path):
                raise errors.BadConfigOption(
                    f"Storage file: {storage_file_path:s} already exists and is not "
                    f"a file."
                )

            if warn_about_existing:
                logger.warning("Appending to an already existing storage file.")

        dirname = os.path.dirname(storage_file_path)
        if not dirname:
            dirname = "."

        # TODO: add a more thorough check to see if the storage file really is
        # a plaso storage file.

        if not os.access(dirname, os.W_OK):
            raise errors.BadConfigOption(
                f"Unable to write to storage file: {storage_file_path:s}"
            )

    def _CreateExtractionEngine(self, single_process_mode):
        """Creates an extraction engine.

        Args:
          single_process_mode (bool): True if the engine should use single process
              mode.

        Returns:
          BaseEngine: extraction engine.
        """
        status_update_callback = self._status_view.GetExtractionStatusUpdateCallback()

        if single_process_mode:
            extraction_engine = single_extraction_engine.SingleProcessEngine(
                status_update_callback=status_update_callback
            )
        else:
            extraction_engine = multi_extraction_engine.ExtractionMultiProcessEngine(
                number_of_worker_processes=self._number_of_extraction_workers,
                status_update_callback=status_update_callback,
                worker_memory_limit=self._worker_memory_limit,
                worker_timeout=self._worker_timeout,
            )

        extraction_engine.SetStatusUpdateInterval(self._status_view_interval)

        return extraction_engine

    def _CreateExtractionProcessingConfiguration(self):
        """Creates an extraction processing configuration.

        Returns:
          ProcessingConfiguration: extraction processing configuration.
        """
        configuration = configurations.ProcessingConfiguration()
        configuration.artifact_definitions_path = self._artifact_definitions_path
        configuration.custom_artifacts_path = self._custom_artifacts_path
        configuration.data_location = self._data_location
        configuration.extraction.archive_types_string = self._archive_types_string
        configuration.artifact_filters = self._artifact_filters
        configuration.credentials = self._credential_configurations
        configuration.debug_output = self._debug_mode
        configuration.extraction.hasher_file_size_limit = self._hasher_file_size_limit
        configuration.extraction.extract_winevt_resources = (
            self._extract_winevt_resources
        )
        configuration.extraction.extract_winreg_binary = self._extract_winreg_binary
        configuration.extraction.hasher_names_string = self._hasher_names_string
        configuration.extraction.process_compressed_streams = (
            self._process_compressed_streams
        )
        configuration.extraction.yara_rules_string = self._yara_rules_string
        configuration.filter_file = self._filter_file
        configuration.log_filename = self._log_file
        configuration.parser_filter_expression = self._expanded_parser_filter_expression
        configuration.preferred_codepage = self._preferred_codepage
        configuration.preferred_language = self._preferred_language
        configuration.preferred_time_zone = self._preferred_time_zone
        configuration.preferred_year = self._preferred_year
        configuration.profiling.directory = self._profiling_directory
        configuration.profiling.sample_rate = self._profiling_sample_rate
        configuration.profiling.profilers = self._profilers
        configuration.task_storage_format = self._task_storage_format
        configuration.temporary_directory = self._temporary_directory

        return configuration

    def _GenerateStorageFileName(self):
        """Generates a name for the storage file.

        The result use a timestamp and the basename of the source path.

        Returns:
          str: a filename for the storage file in the form <time>-<source>.plaso

        Raises:
          BadConfigOption: raised if the source path is not set.
        """
        if not self._source_path:
            raise errors.BadConfigOption("Please define a source (--source).")

        timestamp = datetime.datetime.now()
        datetime_string = timestamp.strftime("%Y%m%dT%H%M%S")

        source_path = os.path.abspath(self._source_path)

        if source_path.endswith(os.path.sep):
            source_path = os.path.dirname(source_path)

        source_name = os.path.basename(source_path)

        if not source_name or source_name in ("/", "\\"):
            # The user passed the filesystem's root as source
            source_name = "ROOT"

        return f"{datetime_string:s}-{source_name:s}.plaso"

    def _GetExpandedParserFilterExpression(self, system_configuration):
        """Determines the expanded parser filter expression.

        Args:
          system_configuration (SystemConfigurationArtifact): system configuration.

        Returns:
          str: expanded parser filter expression.

        Raises:
          BadConfigOption: if presets in the parser filter expression could not
              be expanded or if an invalid parser or plugin name is specified.
        """
        parser_filter_expression = self._parser_filter_expression
        if not parser_filter_expression and system_configuration:
            operating_system_artifact = artifacts.OperatingSystemArtifact(
                family=system_configuration.operating_system,
                product=system_configuration.operating_system_product,
                version=system_configuration.operating_system_version,
            )

            preset_definitions = self._presets_manager.GetPresetsByOperatingSystem(
                operating_system_artifact
            )
            if preset_definitions:
                self._parser_filter_expression = ",".join(
                    [preset_definition.name for preset_definition in preset_definitions]
                )

                logger.debug(
                    (
                        f"Parser filter expression set to preset: "
                        f"{self._parser_filter_expression:s}"
                    )
                )

        parser_filter_helper = parser_filter.ParserFilterExpressionHelper()

        try:
            parser_filter_expression = parser_filter_helper.ExpandPresets(
                self._presets_manager, self._parser_filter_expression
            )
            parser_filter_string = parser_filter_expression or "N/A"
            logger.debug(f"Parser filter expression set to: {parser_filter_string:s}")
        except RuntimeError as exception:
            raise errors.BadConfigOption(
                (
                    f"Unable to expand presets in parser filter expression with "
                    f"error: {exception!s}"
                )
            )

        parser_elements, invalid_parser_elements = (
            parsers_manager.ParsersManager.CheckFilterExpression(
                parser_filter_expression
            )
        )

        if invalid_parser_elements:
            invalid_parser_names_string = ",".join(invalid_parser_elements)
            raise errors.BadConfigOption(
                (
                    f"Unknown parser or plugin names in element(s): "
                    f'"{invalid_parser_names_string:s}" of parser filter expression: '
                    f"{parser_filter_expression:s}"
                )
            )

        return ",".join(sorted(parser_elements))

    def _ParseExtractionOptions(self, options):
        """Parses the extraction options.

        Args:
          options (argparse.Namespace): command line arguments.

        Raises:
          BadConfigOption: if the options are invalid.
        """
        helpers_manager.ArgumentHelperManager.ParseOptions(
            options, self, names=["codepage", "language"]
        )

        # TODO: add preferred encoding

        self.list_language_tags = self._preferred_language == "list"

        self._extract_winevt_resources = getattr(
            options, "extract_winevt_resources", True
        )

        time_zone_string = self.ParseStringOption(options, "timezone")
        if isinstance(time_zone_string, str):
            if time_zone_string.lower() == "list":
                self.list_time_zones = True

            elif time_zone_string:
                try:
                    pytz.timezone(time_zone_string)
                except pytz.UnknownTimeZoneError:
                    raise errors.BadConfigOption(
                        f"Unknown time zone: {time_zone_string:s}"
                    )

                self._preferred_time_zone = time_zone_string

    def _ParsePerformanceOptions(self, options):
        """Parses the performance options.

        Args:
          options (argparse.Namespace): command line arguments.

        Raises:
          BadConfigOption: if the options are invalid.
        """
        self._buffer_size = getattr(options, "buffer_size", 0)
        if self._buffer_size:
            # TODO: turn this into a generic function that supports more size
            # suffixes both MB and MiB and also that does not allow m as a valid
            # indicator for MiB since m represents milli not Mega.
            try:
                if self._buffer_size[-1].lower() == "m":
                    self._buffer_size = int(self._buffer_size[:-1], 10)
                    self._buffer_size *= self._BYTES_IN_A_MIB
                else:
                    self._buffer_size = int(self._buffer_size, 10)
            except ValueError:
                raise errors.BadConfigOption(
                    f"Invalid buffer size: {self._buffer_size!s}."
                )

        self._queue_size = self.ParseNumericOption(options, "queue_size")

    def _ParseProcessingOptions(self, options):
        """Parses the processing options.

        Args:
          options (argparse.Namespace): command line arguments.

        Raises:
          BadConfigOption: if the options are invalid.
        """
        self._single_process_mode = getattr(options, "single_process", False)

        argument_helper_names = [
            "process_resources",
            "temporary_directory",
            "vfs_backend",
            "workers",
            "zeromq",
        ]
        helpers_manager.ArgumentHelperManager.ParseOptions(
            options, self, names=argument_helper_names
        )

        if self._vfs_back_end == "fsext":
            dfvfs_definitions.PREFERRED_EXT_BACK_END = (
                dfvfs_definitions.TYPE_INDICATOR_EXT
            )

        elif self._vfs_back_end == "fsfat":
            dfvfs_definitions.PREFERRED_FAT_BACK_END = (
                dfvfs_definitions.TYPE_INDICATOR_FAT
            )

        elif self._vfs_back_end == "fshfs":
            dfvfs_definitions.PREFERRED_HFS_BACK_END = (
                dfvfs_definitions.TYPE_INDICATOR_HFS
            )

        elif self._vfs_back_end == "fsntfs":
            dfvfs_definitions.PREFERRED_NTFS_BACK_END = (
                dfvfs_definitions.TYPE_INDICATOR_NTFS
            )

        elif self._vfs_back_end == "tsk":
            dfvfs_definitions.PREFERRED_EXT_BACK_END = (
                dfvfs_definitions.TYPE_INDICATOR_TSK
            )
            dfvfs_definitions.PREFERRED_FAT_BACK_END = (
                dfvfs_definitions.TYPE_INDICATOR_TSK
            )
            dfvfs_definitions.PREFERRED_GPT_BACK_END = (
                dfvfs_definitions.TYPE_INDICATOR_TSK_PARTITION
            )
            dfvfs_definitions.PREFERRED_HFS_BACK_END = (
                dfvfs_definitions.TYPE_INDICATOR_TSK
            )
            dfvfs_definitions.PREFERRED_NTFS_BACK_END = (
                dfvfs_definitions.TYPE_INDICATOR_TSK
            )

        elif self._vfs_back_end == "vsgpt":
            dfvfs_definitions.PREFERRED_GPT_BACK_END = (
                dfvfs_definitions.TYPE_INDICATOR_GPT
            )

    def _ProcessSource(self, session, storage_writer):
        """Processes the source and extract events.

        Args:
          session (Session): session in which the source is processed.
          storage_writer (StorageWriter): storage writer to store extracted events.

        Returns:
          ProcessingStatus: processing status.

        Raises:
          BadConfigOption: if an invalid collection filter was specified.
        """
        single_process_mode = self._single_process_mode
        if self._source_type == dfvfs_definitions.SOURCE_TYPE_FILE:
            single_process_mode = True

        extraction_engine = self._CreateExtractionEngine(single_process_mode)

        extraction_engine.BuildArtifactsRegistry(
            self._artifact_definitions_path, self._custom_artifacts_path
        )

        source_configuration = artifacts.SourceConfigurationArtifact(
            path=self._source_path, source_type=self._source_type
        )

        # TODO: check if the source was processed previously.
        # TODO: add check for modification time of source.

        # If the source is a directory or a storage media image run pre-processing.

        system_configurations = []
        if self._source_type in self._SOURCE_TYPES_TO_PREPROCESS:
            try:
                logger.debug("Starting preprocessing.")

                system_configurations = extraction_engine.PreprocessSource(
                    self._file_system_path_specs,
                    storage_writer,
                    resolver_context=self._resolver_context,
                )

                logger.debug("Preprocessing done.")

            except OSError as exception:
                system_configurations = []

                logger.error(f"Unable to preprocess with error: {exception!s}")

            # TODO: check if the source was processed previously and if system
            # configuration differs.

        system_configuration = None
        if system_configurations:
            system_configuration = system_configurations[0]

        # TODO: add support for more than 1 system configuration.
        self._expanded_parser_filter_expression = (
            self._GetExpandedParserFilterExpression(system_configuration)
        )

        enabled_parser_names = self._expanded_parser_filter_expression.split(",")

        number_of_enabled_parsers = len(enabled_parser_names)

        force_parser = False
        if (
            self._source_type == dfvfs_definitions.SOURCE_TYPE_FILE
            and number_of_enabled_parsers == 1
        ):
            force_parser = True

            self._extract_winevt_resources = False

        elif (
            "winevt" not in enabled_parser_names
            and "winevtx" not in enabled_parser_names
        ):
            self._extract_winevt_resources = False

        elif self._extract_winevt_resources and "pe" not in enabled_parser_names:
            logger.warning(
                "A Windows EventLog parser is enabled in combination with "
                "extraction of Windows EventLog resources, but the Portable "
                "Executable (PE) parser is disabled. Therefore Windows EventLog "
                "resources cannot be extracted."
            )

            self._extract_winevt_resources = False

        processing_configuration = self._CreateExtractionProcessingConfiguration()
        processing_configuration.force_parser = force_parser

        environment_variables = (
            extraction_engine.knowledge_base.GetEnvironmentVariables()
        )
        user_accounts = list(storage_writer.GetAttributeContainers("user_account"))

        try:
            extraction_engine.BuildCollectionFilters(
                environment_variables,
                user_accounts,
                artifact_filter_names=self._artifact_filters,
                filter_file_path=self._filter_file,
            )
        except errors.InvalidFilter as exception:
            raise errors.BadConfigOption(
                f"Unable to build collection filters with error: {exception!s}"
            )

        session.artifact_filters = self._artifact_filters
        session.command_line_arguments = self._command_line_arguments
        session.debug_mode = self._debug_mode
        session.enabled_parser_names = enabled_parser_names
        session.extract_winevt_resources = self._extract_winevt_resources
        session.filter_file = self._filter_file
        session.parser_filter_expression = self._parser_filter_expression
        session.preferred_codepage = self._preferred_codepage
        session.preferred_encoding = self.preferred_encoding
        session.preferred_language = self._preferred_language or "en-US"
        session.preferred_time_zone = self._preferred_time_zone
        session.preferred_year = self._preferred_year

        storage_writer.AddAttributeContainer(session)

        processing_status = None

        try:
            storage_writer.AddAttributeContainer(source_configuration)

            for system_configuration in system_configurations:
                storage_writer.AddAttributeContainer(system_configuration)

            if single_process_mode:
                logger.debug("Starting extraction in single process mode.")

                processing_status = extraction_engine.ProcessSource(
                    storage_writer,
                    self._resolver_context,
                    processing_configuration,
                    system_configurations,
                    self._file_system_path_specs,
                )

            else:
                logger.debug("Starting extraction in multi process mode.")

                # The method is named ProcessSourceMulti because pylint 2.6.0 and
                # later gets confused about keyword arguments when ProcessSource
                # is used.
                processing_status = extraction_engine.ProcessSourceMulti(
                    storage_writer,
                    session.identifier,
                    processing_configuration,
                    system_configurations,
                    self._file_system_path_specs,
                    enable_sigsegv_handler=self._enable_sigsegv_handler,
                    storage_file_path=self._storage_file_path,
                )

        finally:
            session.aborted = getattr(processing_status, "aborted", True)
            session.completion_time = int(time.time() * 1000000)
            storage_writer.UpdateAttributeContainer(session)

        return processing_status

    def _ReadParserPresetsFromFile(self):
        """Reads the parser presets from the presets.yaml file.

        Raises:
          BadConfigOption: if the parser presets file cannot be read.
        """
        self._presets_file = os.path.join(self._data_location, self._PRESETS_FILE_NAME)
        if not os.path.isfile(self._presets_file):
            raise errors.BadConfigOption(
                f"No such parser presets file: {self._presets_file:s}"
            )

        try:
            self._presets_manager.ReadFromFile(self._presets_file)
        except errors.MalformedPresetError as exception:
            raise errors.BadConfigOption(
                f"Unable to read parser presets from file with error: {exception!s}"
            )

    def _ScanSourceForArchive(self, path_spec):
        """Determines if a path specification references an archive file.

        Args:
          path_spec (dfvfs.PathSpec): path specification of the data stream.

        Returns:
          dfvfs.PathSpec: path specification of the archive file or None if not
              an archive file.
        """
        try:
            type_indicators = dfvfs_analyzer.Analyzer.GetCompressedStreamTypeIndicators(
                path_spec, resolver_context=self._resolver_context
            )
        except OSError:
            type_indicators = []

        if len(type_indicators) > 1:
            return False

        if type_indicators:
            type_indicator = type_indicators[0]
        else:
            type_indicator = None

        if type_indicator == dfvfs_definitions.TYPE_INDICATOR_BZIP2:
            path_spec = path_spec_factory.Factory.NewPathSpec(
                dfvfs_definitions.TYPE_INDICATOR_COMPRESSED_STREAM,
                compression_method=dfvfs_definitions.COMPRESSION_METHOD_BZIP2,
                parent=path_spec,
            )

        elif type_indicator == dfvfs_definitions.TYPE_INDICATOR_GZIP:
            path_spec = path_spec_factory.Factory.NewPathSpec(
                dfvfs_definitions.TYPE_INDICATOR_GZIP, parent=path_spec
            )

        elif type_indicator == dfvfs_definitions.TYPE_INDICATOR_XZ:
            path_spec = path_spec_factory.Factory.NewPathSpec(
                dfvfs_definitions.TYPE_INDICATOR_COMPRESSED_STREAM,
                compression_method=dfvfs_definitions.COMPRESSION_METHOD_XZ,
                parent=path_spec,
            )

        try:
            type_indicators = dfvfs_analyzer.Analyzer.GetArchiveTypeIndicators(
                path_spec, resolver_context=self._resolver_context
            )
        except OSError:
            return None

        if len(type_indicators) != 1:
            return None

        return path_spec_factory.Factory.NewPathSpec(
            type_indicators[0], location="/", parent=path_spec
        )

    def AddExtractionOptions(self, argument_group):
        """Adds the extraction options to the argument group.

        Args:
          argument_group (argparse._ArgumentGroup): argparse argument group.
        """
        helpers_manager.ArgumentHelperManager.AddCommandLineArguments(
            argument_group, names=["codepage", "language"]
        )

        # Note defaults here are None so we can determine if an option was set.

        argument_group.add_argument(
            "--no_extract_winevt_resources",
            "--no-extract-winevt-resources",
            dest="extract_winevt_resources",
            action="store_false",
            default=True,
            help=(
                "Do not extract Windows EventLog resources such as event "
                "message template strings. By default Windows EventLog "
                "resources will be extracted when a Windows EventLog parser "
                "is enabled."
            ),
        )

        # TODO: add preferred encoding

        argument_group.add_argument(
            "-z",
            "--zone",
            "--timezone",
            dest="timezone",
            action="store",
            metavar="TIME_ZONE",
            type=str,
            default=None,
            help=(
                "preferred time zone of extracted date and time values that are "
                "stored without a time zone indicator. The time zone is determined "
                "based on the source data where possible otherwise it will default "
                'to UTC. Use "list" to see a list of available time zones.'
            ),
        )

    def AddPerformanceOptions(self, argument_group):
        """Adds the performance options to the argument group.

        Args:
          argument_group (argparse._ArgumentGroup): argparse argument group.
        """
        argument_group.add_argument(
            "--buffer_size",
            "--buffer-size",
            "--bs",
            dest="buffer_size",
            action="store",
            default=0,
            help=("The buffer size for the output (defaults to 196MiB)."),
        )

        argument_group.add_argument(
            "--queue_size",
            "--queue-size",
            dest="queue_size",
            action="store",
            default=0,
            help=(
                f"The maximum number of queued items per worker (defaults to "
                f"{self._DEFAULT_QUEUE_SIZE:d})"
            ),
        )

    def AddProcessingOptions(self, argument_group):
        """Adds the processing options to the argument group.

        Args:
          argument_group (argparse._ArgumentGroup): argparse argument group.
        """
        argument_group.add_argument(
            "--single_process",
            "--single-process",
            dest="single_process",
            action="store_true",
            default=False,
            help=("Indicate that the tool should run in a single process."),
        )

        argument_helper_names = [
            "temporary_directory",
            "vfs_backend",
            "workers",
            "zeromq",
        ]
        if self._CanEnforceProcessMemoryLimit():
            argument_helper_names.append("process_resources")
        helpers_manager.ArgumentHelperManager.AddCommandLineArguments(
            argument_group, names=argument_helper_names
        )

    def ExtractEventsFromSources(self):
        """Processes the sources and extracts events.

        Raises:
          BadConfigOption: if the storage file path is invalid, or the storage
              format not supported, or there was a failure to writing to the
              storage.
          OSError: if the extraction engine could not write to the storage.
          SourceScannerError: if the source scanner could not find a supported
              file system.
          UserAbort: if the user initiated an abort.
        """
        self._CheckStorageFile(self._storage_file_path, warn_about_existing=True)

        try:
            self.ScanSource(self._source_path)
        except dfvfs_errors.UserAbort as exception:
            raise errors.UserAbort(exception)

        if self._source_type == dfvfs_definitions.SOURCE_TYPE_FILE:
            archive_path_spec = self._ScanSourceForArchive(
                self._file_system_path_specs[0]
            )
            if archive_path_spec:
                self._file_system_path_specs = [archive_path_spec]
                self._source_type = definitions.SOURCE_TYPE_ARCHIVE

        self._status_view.SetMode(self._status_view_mode)
        self._status_view.SetStatusFile(self._status_view_file)
        self._status_view.SetSourceInformation(
            self._source_path,
            self._source_type,
            artifact_filters=self._artifact_filters,
            filter_file=self._filter_file,
        )

        self._output_writer.Write("\n")
        self._status_view.PrintExtractionStatusHeader(None)
        self._output_writer.Write("Processing started.\n")

        # TODO: attach processing configuration to session?
        session = engine.BaseEngine.CreateSession()

        storage_writer = storage_factory.StorageFactory.CreateStorageWriter(
            self._storage_format
        )
        if not storage_writer:
            raise errors.BadConfigOption(
                f"Unsupported storage format: {self._storage_format:s}"
            )

        try:
            storage_writer.Open(path=self._storage_file_path)
        except OSError as exception:
            raise OSError(f"Unable to open storage with error: {exception!s}")

        processing_status = None
        number_of_extraction_warnings = 0

        try:
            stored_number_of_extraction_warnings = (
                storage_writer.GetNumberOfAttributeContainers("extraction_warning")
            )

            try:
                processing_status = self._ProcessSource(session, storage_writer)

            finally:
                number_of_extraction_warnings = (
                    storage_writer.GetNumberOfAttributeContainers("extraction_warning")
                    - stored_number_of_extraction_warnings
                )

        except OSError as exception:
            raise OSError(f"Unable to write to storage with error: {exception!s}")

        finally:
            storage_writer.Close()

        self._status_view.PrintExtractionSummary(
            processing_status, number_of_extraction_warnings
        )

    def ListArchiveTypes(self):
        """Lists information about supported archive types."""
        table_view = views.ViewsFactory.GetTableView(
            self._views_format_type,
            column_names=["Name", "Description"],
            title="Archive and storage media image types",
        )

        for name, description in sorted(self._SUPPORTED_ARCHIVE_TYPES.items()):
            table_view.AddRow([name, description])

        table_view.Write(self._output_writer)

    def ListLanguageTags(self):
        """Lists the language tags."""
        table_view = views.ViewsFactory.GetTableView(
            self._views_format_type,
            column_names=["Language tag", "Description"],
            title="Language tags",
        )
        for language_tag, description in language_tags.LanguageTagHelper.GetLanguages():
            table_view.AddRow([language_tag, description])
        table_view.Write(self._output_writer)

    def ListParsersAndPlugins(self):
        """Lists information about the available parsers and plugins."""
        parsers_information = parsers_manager.ParsersManager.GetParsersInformation()

        table_view = views.ViewsFactory.GetTableView(
            self._views_format_type,
            column_names=["Name", "Description"],
            title="Parsers",
        )

        for name, description in sorted(parsers_information):
            table_view.AddRow([name, description])
        table_view.Write(self._output_writer)

        parser_names = parsers_manager.ParsersManager.GetNamesOfParsersWithPlugins()
        for parser_name in parser_names:
            plugins_information = (
                parsers_manager.ParsersManager.GetParserPluginsInformation(
                    parser_filter_expression=parser_name
                )
            )

            table_title = f"Parser plugins: {parser_name:s}"
            table_view = views.ViewsFactory.GetTableView(
                self._views_format_type,
                column_names=["Name", "Description"],
                title=table_title,
            )
            for name, description in sorted(plugins_information):
                table_view.AddRow([name, description])
            table_view.Write(self._output_writer)

        title = "Parser presets"
        if self._presets_file:
            source_path = os.path.dirname(
                os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
            )

            presets_file = self._presets_file
            if presets_file.startswith(source_path):
                presets_file = presets_file[len(source_path) + 1 :]

            title = f"{title:s} ({presets_file:s})"

        presets_information = self._presets_manager.GetPresetsInformation()

        table_view = views.ViewsFactory.GetTableView(
            self._views_format_type,
            column_names=["Name", "Parsers and plugins"],
            title=title,
        )
        for name, description in sorted(presets_information):
            table_view.AddRow([name, description])
        table_view.Write(self._output_writer)
