#!/usr/bin/env python

"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""

import hashlib
import json
import os
import re
import shutil
import tempfile
import threading

from lib.core.common import Backend
from lib.core.common import checkFile
from lib.core.common import clearColors
from lib.core.common import dataToDumpFile
from lib.core.common import dataToStdout
from lib.core.common import filterNone
from lib.core.common import getSafeExString
from lib.core.common import isListLike
from lib.core.common import isNoneValue
from lib.core.common import normalizeUnicode
from lib.core.common import openFile
from lib.core.common import prioritySortColumns
from lib.core.common import randomInt
from lib.core.common import safeCSValue
from lib.core.common import unArrayizeValue
from lib.core.common import unsafeSQLIdentificatorNaming
from lib.core.compat import xrange
from lib.core.convert import getBytes
from lib.core.convert import getConsoleLength
from lib.core.convert import stdoutEncode
from lib.core.convert import getText
from lib.core.convert import getUnicode
from lib.core.convert import htmlEscape
from lib.core.data import conf
from lib.core.data import kb
from lib.core.data import logger
from lib.core.dicts import DUMP_REPLACEMENTS
from lib.core.enums import CONTENT_STATUS
from lib.core.enums import CONTENT_TYPE
from lib.core.enums import DBMS
from lib.core.enums import DUMP_FORMAT
from lib.core.exception import SqlmapGenericException
from lib.core.exception import SqlmapSystemException
from lib.core.exception import SqlmapValueException
from lib.core.replication import Replication
from lib.core.settings import CHECK_SQLITE_TYPE_THRESHOLD
from lib.core.settings import DUMP_FILE_BUFFER_SIZE
from lib.core.settings import HTML_DUMP_CSS_STYLE
from lib.core.settings import IS_WIN
from lib.core.settings import METADB_SUFFIX
from lib.core.settings import MIN_BINARY_DISK_DUMP_SIZE
from lib.core.settings import TRIM_STDOUT_DUMP_SIZE
from lib.core.settings import UNICODE_ENCODING
from lib.core.settings import UNSAFE_DUMP_FILEPATH_REPLACEMENT
from lib.core.settings import VERSION_STRING
from lib.core.settings import WINDOWS_RESERVED_NAMES
from lib.utils.safe2bin import safechardecode
from thirdparty import six
from thirdparty.magic import magic
from collections import OrderedDict

class Dump(object):
    """
    This class defines methods used to parse and output the results
    of SQL injection actions
    """

    def __init__(self):
        self._outputFile = None
        self._outputFP = None
        self._lock = threading.Lock()

    def _write(self, data, newline=True, console=True, content_type=None):
        text = "%s%s" % (data, "\n" if newline else " ")

        if conf.api:
            dataToStdout(data, contentType=content_type, status=CONTENT_STATUS.COMPLETE)

        elif console:
            dataToStdout(text)

        if self._outputFP:
            multiThreadMode = kb.multiThreadMode
            if multiThreadMode:
                self._lock.acquire()

            try:
                self._outputFP.write(text)
            except IOError as ex:
                errMsg = "error occurred while writing to log file ('%s')" % getSafeExString(ex)
                raise SqlmapGenericException(errMsg)
            finally:
                if multiThreadMode:
                    self._lock.release()

        kb.dataOutputFlag = True

    def _reportData(self, data, content_type):
        """
        --report-json: capture a structured result exactly as the REST API would store it (the raw
        value + COMPLETE status), independent of console/file rendering. No-op unless a report
        collector is active - which is only ever the case for a CLI --report-json run, never under
        --api - so this never double-captures alongside StdDbOut. A None content_type is resolved
        via the kb.partRun fallback (e.g. the fingerprint line), mirroring the API exactly.
        """

        if conf.get("reportCollector") is not None:
            from lib.utils.api import _storeData, REPORT_TASKID
            _storeData(conf.reportCollector, REPORT_TASKID, stdoutEncode(clearColors(data)), CONTENT_STATUS.COMPLETE, content_type)

    def flush(self):
        if self._outputFP:
            try:
                self._outputFP.flush()
            except IOError:
                pass

    def setOutputFile(self):
        if conf.noLogging:
            self._outputFP = None
            return

        self._outputFile = os.path.join(conf.outputPath, "log")
        try:
            self._outputFP = openFile(self._outputFile, 'a' if not conf.flushSession else 'w')
        except IOError as ex:
            errMsg = "error occurred while opening log file ('%s')" % getSafeExString(ex)
            raise SqlmapGenericException(errMsg)

    def singleString(self, data, content_type=None):
        self._reportData(data, content_type)
        self._write(data, content_type=content_type)

    def string(self, header, data, content_type=None, sort=True):
        self._reportData(data, content_type)

        if conf.api:
            self._write(data, content_type=content_type)

        if isListLike(data) and len(data) == 1:
            data = unArrayizeValue(data)

        if isListLike(data):
            self.lister(header, data, content_type, sort)
        elif data is not None:
            _ = getUnicode(data)

            if _.endswith("\r\n"):
                _ = _[:-2]

            elif _.endswith("\n"):
                _ = _[:-1]

            if _.strip(' '):
                _ = _.strip(' ')

            if "\n" in _:
                self._write("%s:\n---\n%s\n---" % (header, _))
            else:
                self._write("%s: %s" % (header, ("'%s'" % _) if isinstance(data, six.string_types) else _))

    def lister(self, header, elements, content_type=None, sort=True):
        if elements and sort:
            try:
                elements = set(elements)
                elements = list(elements)
                elements.sort(key=lambda _: _.lower() if hasattr(_, "lower") else _)
            except:
                pass

        self._reportData(elements, content_type)

        if conf.api:
            self._write(elements, content_type=content_type)

        if elements:
            self._write("%s [%d]:" % (header, len(elements)))

        for element in elements:
            if isinstance(element, six.string_types):
                self._write("[*] %s" % element)
            elif isListLike(element):
                self._write("[*] " + ", ".join(getUnicode(e) for e in element))

        if elements:
            self._write("")

    def banner(self, data):
        self.string("banner", data, content_type=CONTENT_TYPE.BANNER)

    def currentUser(self, data):
        self.string("current user", data, content_type=CONTENT_TYPE.CURRENT_USER)

    def currentDb(self, data):
        if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.MONETDB, DBMS.VERTICA, DBMS.CRATEDB, DBMS.CACHE, DBMS.FRONTBASE, DBMS.SNOWFLAKE, DBMS.HANA):
            self.string("current database (equivalent to schema on %s)" % Backend.getIdentifiedDbms(), data, content_type=CONTENT_TYPE.CURRENT_DB)
        elif Backend.getIdentifiedDbms() in (DBMS.ALTIBASE, DBMS.DB2, DBMS.MIMERSQL, DBMS.MAXDB, DBMS.VIRTUOSO):
            self.string("current database (equivalent to owner on %s)" % Backend.getIdentifiedDbms(), data, content_type=CONTENT_TYPE.CURRENT_DB)
        else:
            self.string("current database", data, content_type=CONTENT_TYPE.CURRENT_DB)

    def hostname(self, data):
        self.string("hostname", data, content_type=CONTENT_TYPE.HOSTNAME)

    def dba(self, data):
        self.string("current user is DBA", data, content_type=CONTENT_TYPE.IS_DBA)

    def users(self, users):
        self.lister("database management system users", users, content_type=CONTENT_TYPE.USERS)

    def statements(self, statements):
        self.lister("SQL statements", statements, content_type=CONTENT_TYPE.STATEMENTS)

    def procedures(self, procedures):
        self.lister("stored procedures", procedures, content_type=CONTENT_TYPE.PROCEDURES)

    def userSettings(self, header, userSettings, subHeader, content_type=None):
        self._areAdmins = set()

        if isinstance(userSettings, (tuple, list, set)):
            self._areAdmins = userSettings[1]
            userSettings = userSettings[0]

        users = [_ for _ in userSettings.keys() if _ is not None]
        users.sort(key=lambda _: _.lower() if hasattr(_, "lower") else _)

        self._reportData(userSettings, content_type)

        if conf.api:
            self._write(userSettings, content_type=content_type)

        if userSettings:
            self._write("%s:" % header)

        for user in users:
            settings = filterNone(userSettings[user])

            if isNoneValue(settings):
                stringSettings = ""
            else:
                stringSettings = " [%d]:" % len(settings)

            if user in self._areAdmins:
                self._write("[*] %s (administrator)%s" % (user, stringSettings))
            else:
                self._write("[*] %s%s" % (user, stringSettings))

            if settings:
                settings.sort()

                for setting in settings:
                    self._write("    %s: %s" % (subHeader, setting))

        if userSettings:
            self.singleString("")

    def dbs(self, dbs):
        self.lister("available databases", dbs, content_type=CONTENT_TYPE.DBS)

    def dbTables(self, dbTables):
        if isinstance(dbTables, dict) and len(dbTables) > 0:
            self._reportData(dbTables, CONTENT_TYPE.TABLES)

            if conf.api:
                self._write(dbTables, content_type=CONTENT_TYPE.TABLES)

            maxlength = 0

            for tables in dbTables.values():
                for table in tables:
                    if table and isListLike(table):
                        table = table[0]

                    maxlength = max(maxlength, getConsoleLength(unsafeSQLIdentificatorNaming(getUnicode(table))))

            lines = "-" * (int(maxlength) + 2)

            for db, tables in dbTables.items():
                tables = sorted(filter(None, tables))

                self._write("Database: %s" % unsafeSQLIdentificatorNaming(db) if db and METADB_SUFFIX not in db else "<current>")

                if len(tables) == 1:
                    self._write("[1 table]")
                else:
                    self._write("[%d tables]" % len(tables))

                self._write("+%s+" % lines)

                for table in tables:
                    if table and isListLike(table):
                        table = table[0]

                    table = unsafeSQLIdentificatorNaming(table)
                    blank = " " * (maxlength - getConsoleLength(getUnicode(table)))
                    self._write("| %s%s |" % (table, blank))

                self._write("+%s+\n" % lines)
        elif dbTables is None or len(dbTables) == 0:
            self.singleString("No tables found", content_type=CONTENT_TYPE.TABLES)
        else:
            self.string("tables", dbTables, content_type=CONTENT_TYPE.TABLES)

    def dbTableColumns(self, tableColumns, content_type=None):
        if isinstance(tableColumns, dict) and len(tableColumns) > 0:
            self._reportData(tableColumns, content_type)

            if conf.api:
                self._write(tableColumns, content_type=content_type)

            for db, tables in tableColumns.items():
                if not db:
                    db = "All"

                for table, columns in tables.items():
                    maxlength1 = 0
                    maxlength2 = 0

                    colList = list(columns.keys())
                    colList.sort(key=lambda _: _.lower() if hasattr(_, "lower") else _)

                    # Note: decide the layout by whether ANY column carries a type, not by the last
                    # column iterated; otherwise a mixed table (some columns typed, some not) whose
                    # alphabetically-last column is type-less renders a header/body column mismatch
                    hasType = any(columns[_] is not None for _ in colList)

                    for column in colList:
                        colType = columns[column]

                        column = unsafeSQLIdentificatorNaming(column)
                        maxlength1 = max(maxlength1, getConsoleLength(column or ""))
                        maxlength2 = max(maxlength2, getConsoleLength(colType or ""))

                    maxlength1 = max(maxlength1, len("COLUMN"))
                    lines1 = "-" * (maxlength1 + 2)

                    if hasType:
                        maxlength2 = max(maxlength2, len("TYPE"))
                        lines2 = "-" * (maxlength2 + 2)

                    self._write("Database: %s\nTable: %s" % (unsafeSQLIdentificatorNaming(db) if db and METADB_SUFFIX not in db else "<current>", unsafeSQLIdentificatorNaming(table)))

                    if len(columns) == 1:
                        self._write("[1 column]")
                    else:
                        self._write("[%d columns]" % len(columns))

                    if hasType:
                        self._write("+%s+%s+" % (lines1, lines2))
                    else:
                        self._write("+%s+" % lines1)

                    blank1 = " " * (maxlength1 - len("COLUMN"))

                    if hasType:
                        blank2 = " " * (maxlength2 - len("TYPE"))
                        self._write("| Column%s | Type%s |" % (blank1, blank2))
                        self._write("+%s+%s+" % (lines1, lines2))
                    else:
                        self._write("| Column%s |" % blank1)
                        self._write("+%s+" % lines1)

                    for column in colList:
                        colType = columns[column]

                        column = unsafeSQLIdentificatorNaming(column)
                        blank1 = " " * (maxlength1 - getConsoleLength(column))

                        if hasType:
                            colType = colType or ""
                            blank2 = " " * (maxlength2 - getConsoleLength(colType))
                            self._write("| %s%s | %s%s |" % (column, blank1, colType, blank2))
                        else:
                            self._write("| %s%s |" % (column, blank1))

                    if hasType:
                        self._write("+%s+%s+\n" % (lines1, lines2))
                    else:
                        self._write("+%s+\n" % lines1)

    def dbTablesCount(self, dbTables):
        if isinstance(dbTables, dict) and len(dbTables) > 0:
            self._reportData(dbTables, CONTENT_TYPE.COUNT)

            if conf.api:
                self._write(dbTables, content_type=CONTENT_TYPE.COUNT)

            maxlength1 = len("Table")
            maxlength2 = len("Entries")

            for ctables in dbTables.values():
                for tables in ctables.values():
                    for table in tables:
                        maxlength1 = max(maxlength1, getConsoleLength(getUnicode(table)))

            for db, counts in dbTables.items():
                self._write("Database: %s" % unsafeSQLIdentificatorNaming(db) if db and METADB_SUFFIX not in db else "<current>")

                lines1 = "-" * (maxlength1 + 2)
                blank1 = " " * (maxlength1 - len("Table"))
                lines2 = "-" * (maxlength2 + 2)
                blank2 = " " * (maxlength2 - len("Entries"))

                self._write("+%s+%s+" % (lines1, lines2))
                self._write("| Table%s | Entries%s |" % (blank1, blank2))
                self._write("+%s+%s+" % (lines1, lines2))

                sortedCounts = list(counts.keys())
                sortedCounts.sort(reverse=True)

                for count in sortedCounts:
                    tables = counts[count]

                    if count is None:
                        count = "Unknown"

                    tables.sort(key=lambda _: _.lower() if hasattr(_, "lower") else _)

                    for table in tables:
                        blank1 = " " * (maxlength1 - getConsoleLength(getUnicode(table)))
                        blank2 = " " * (maxlength2 - len(str(count)))
                        self._write("| %s%s | %d%s |" % (table, blank1, count, blank2))

                self._write("+%s+%s+\n" % (lines1, lines2))
        else:
            logger.error("unable to retrieve the number of entries for any table")

    def dbTableValues(self, tableValues):
        replication = None
        rtable = None
        dumpFP = None
        appendToFile = False
        warnFile = False

        if tableValues is None:
            return

        db = tableValues["__infos__"]["db"]
        if not db:
            db = "All"
        table = tableValues["__infos__"]["table"]

        safeDb = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, unsafeSQLIdentificatorNaming(db))
        safeTable = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, unsafeSQLIdentificatorNaming(table))

        self._reportData(tableValues, CONTENT_TYPE.DUMP_TABLE)

        if conf.api:
            self._write(tableValues, content_type=CONTENT_TYPE.DUMP_TABLE)

        try:
            dumpDbPath = os.path.join(conf.dumpPath, safeDb)
        except UnicodeError:
            try:
                dumpDbPath = os.path.join(conf.dumpPath, normalizeUnicode(safeDb))
            except (UnicodeError, OSError):
                tempDir = tempfile.mkdtemp(prefix="sqlmapdb")
                warnMsg = "currently unable to use regular dump directory. "
                warnMsg += "Using temporary directory '%s' instead" % tempDir
                logger.warning(warnMsg)

                dumpDbPath = tempDir

        if conf.dumpFormat == DUMP_FORMAT.SQLITE:
            replication = Replication(os.path.join(conf.dumpPath, "%s.sqlite3" % safeDb))
        elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML, DUMP_FORMAT.JSONL):
            if not os.path.isdir(dumpDbPath):
                try:
                    os.makedirs(dumpDbPath)
                except:
                    warnFile = True
                    dumpDbPath = os.path.join(conf.dumpPath, "%s-%s" % (safeDb, hashlib.md5(getBytes(db)).hexdigest()[:8]))

                    if not os.path.isdir(dumpDbPath):
                        try:
                            os.makedirs(dumpDbPath)
                        except Exception as ex:
                            tempDir = tempfile.mkdtemp(prefix="sqlmapdb")
                            warnMsg = "unable to create dump directory "
                            warnMsg += "'%s' (%s). " % (dumpDbPath, getSafeExString(ex))
                            warnMsg += "Using temporary directory '%s' instead" % tempDir
                            logger.warning(warnMsg)

                            dumpDbPath = tempDir

            dumpFileName = conf.dumpFile or os.path.join(dumpDbPath, "%s.%s" % (safeTable, conf.dumpFormat.lower()))

            if not checkFile(dumpFileName, False):
                try:
                    openFile(dumpFileName, "w+").close()
                except SqlmapSystemException:
                    raise
                except:
                    warnFile = True
                    if IS_WIN and safeTable.upper() in WINDOWS_RESERVED_NAMES:
                        dumpFileName = os.path.join(dumpDbPath, "%s-%s.%s" % (safeTable, hashlib.md5(getBytes(table)).hexdigest()[:8], conf.dumpFormat.lower()))
                    else:
                        dumpFileName = os.path.join(dumpDbPath, "%s.%s" % (safeTable, conf.dumpFormat.lower()))
            else:
                appendToFile = any((conf.limitStart, conf.limitStop))

                if not appendToFile:
                    count = 1
                    while True:
                        candidate = "%s.%d" % (dumpFileName, count)
                        if not checkFile(candidate, False):
                            try:
                                shutil.copyfile(dumpFileName, candidate)
                            except IOError:
                                pass
                            break
                        else:
                            count += 1

            dumpFP = openFile(dumpFileName, 'w' if not appendToFile else 'a', buffering=DUMP_FILE_BUFFER_SIZE)

        count = int(tableValues["__infos__"]["count"])
        if count > TRIM_STDOUT_DUMP_SIZE:
            warnMsg = "console output will be trimmed to "
            warnMsg += "last %d rows due to " % TRIM_STDOUT_DUMP_SIZE
            warnMsg += "large table size"
            logger.warning(warnMsg)

        separator = str()
        field = 1
        fields = len(tableValues) - 1

        columns = prioritySortColumns(list(tableValues.keys()))

        if conf.col:
            cols = conf.col.split(',')
            columns = sorted(columns, key=lambda _: cols.index(_) if _ in cols else 0)

        for column in columns:
            if column != "__infos__":
                info = tableValues[column]
                lines = "-" * (int(info["length"]) + 2)
                separator += "+%s" % lines

        separator += "+"
        self._write("Database: %s\nTable: %s" % (unsafeSQLIdentificatorNaming(db) if db and METADB_SUFFIX not in db else "<current>", unsafeSQLIdentificatorNaming(table)))

        if conf.dumpFormat == DUMP_FORMAT.SQLITE:
            cols = []

            for column in columns:
                if column != "__infos__":
                    colType = Replication.INTEGER

                    for i in xrange(min(CHECK_SQLITE_TYPE_THRESHOLD, len(tableValues[column]['values']))):
                        value = tableValues[column]['values'][i]
                        try:
                            if not value or value == " ":  # NULL
                                continue

                            int(value)
                        except ValueError:
                            colType = None
                            break

                    if colType is None:
                        colType = Replication.REAL

                        for i in xrange(min(CHECK_SQLITE_TYPE_THRESHOLD, len(tableValues[column]['values']))):
                            value = tableValues[column]['values'][i]
                            try:
                                if not value or value == " ":  # NULL
                                    continue

                                float(value)
                            except ValueError:
                                colType = None
                                break

                    cols.append((unsafeSQLIdentificatorNaming(column), colType if colType else Replication.TEXT))

            rtable = replication.createTable(table, cols)
        elif conf.dumpFormat == DUMP_FORMAT.HTML:
            dataToDumpFile(dumpFP, "<!DOCTYPE html>\n<html>\n<head>\n")
            dataToDumpFile(dumpFP, "<meta http-equiv=\"Content-type\" content=\"text/html;charset=%s\">\n" % UNICODE_ENCODING)
            dataToDumpFile(dumpFP, "<meta name=\"generator\" content=\"%s\" />\n" % VERSION_STRING)
            dataToDumpFile(dumpFP, "<title>%s</title>\n" % ("%s%s" % ("%s." % db if METADB_SUFFIX not in db else "", table)).replace("<", ""))
            dataToDumpFile(dumpFP, HTML_DUMP_CSS_STYLE)
            dataToDumpFile(dumpFP, "\n</head>\n<body>\n<table>\n<thead>\n<tr>\n")

        if count == 1:
            self._write("[1 entry]")
        else:
            self._write("[%d entries]" % count)

        self._write(separator)

        for column in columns:
            if column != "__infos__":
                info = tableValues[column]

                column = unsafeSQLIdentificatorNaming(column)
                maxlength = int(info["length"])
                blank = " " * (maxlength - getConsoleLength(column))

                self._write("| %s%s" % (column, blank), newline=False)

                if not appendToFile:
                    if conf.dumpFormat == DUMP_FORMAT.CSV:
                        if field == fields:
                            dataToDumpFile(dumpFP, "%s" % safeCSValue(column))
                        else:
                            dataToDumpFile(dumpFP, "%s%s" % (safeCSValue(column), conf.csvDel))
                    elif conf.dumpFormat == DUMP_FORMAT.HTML:
                        dataToDumpFile(dumpFP, "<th onclick=\"sortTable(%d,this)\">%s</th>" % (field - 1, getUnicode(htmlEscape(column).encode("ascii", "xmlcharrefreplace"))))

                field += 1

        if conf.dumpFormat == DUMP_FORMAT.HTML:
            dataToDumpFile(dumpFP, "\n</tr>\n</thead>\n<tbody>\n")

        self._write("|\n%s" % separator)

        if conf.dumpFormat == DUMP_FORMAT.CSV:
            dataToDumpFile(dumpFP, "\n" if not appendToFile else "")

        elif conf.dumpFormat == DUMP_FORMAT.SQLITE:
            rtable.beginTransaction()

        # Precompute the per-column layout once. These values are invariant across
        # every row, so resolving them per cell (dict lookup, int() conversion and
        # identifier normalization) wasted count x ncols work on large dumps.
        dumpColumns = []
        for column in columns:
            if column != "__infos__":
                info = tableValues[column]
                dumpColumns.append((unsafeSQLIdentificatorNaming(column), info["values"], int(info["length"])))

        for i in xrange(count):
            console = (i >= count - TRIM_STDOUT_DUMP_SIZE)
            field = 1

            # Only the SQLITE and JSONL paths accumulate a per-row container; the
            # others left these unused, wasting an allocation on every single row
            if conf.dumpFormat == DUMP_FORMAT.SQLITE:
                values = []
            elif conf.dumpFormat == DUMP_FORMAT.JSONL:
                record = OrderedDict()

            if i == 0 and count > TRIM_STDOUT_DUMP_SIZE:
                self._write(" ...")

            if conf.dumpFormat == DUMP_FORMAT.HTML:
                dataToDumpFile(dumpFP, "<tr>")

            for safeColumn, colValues, maxlength in dumpColumns:
                if len(colValues) <= i or colValues[i] is None:
                    value = u''
                else:
                    value = getUnicode(colValues[i])
                    value = DUMP_REPLACEMENTS.get(value, value)

                if conf.dumpFormat == DUMP_FORMAT.SQLITE:
                    # Note: store a real NULL for the NULL sentinel (and the raw value otherwise),
                    # mirroring the JSONL path below; appending the display-replaced 'NULL'/'<blank>'
                    # text would corrupt the INTEGER/REAL-typed columns inferred above
                    if len(colValues) <= i or colValues[i] is None or colValues[i] == " ":  # NULL
                        values.append(None)
                    else:
                        values.append(getUnicode(colValues[i]))

                blank = " " * (maxlength - getConsoleLength(value))
                self._write("| %s%s" % (value, blank), newline=False, console=console)

                if len(value) > MIN_BINARY_DISK_DUMP_SIZE and r'\x' in value:
                    try:
                        mimetype = getText(magic.from_buffer(getBytes(value), mime=True))
                        if any(mimetype.startswith(_) for _ in ("application", "image")):
                            if not os.path.isdir(dumpDbPath):
                                os.makedirs(dumpDbPath)

                            _ = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, normalizeUnicode(safeColumn))
                            filepath = os.path.join(dumpDbPath, "%s-%d.bin" % (_, randomInt(8)))
                            warnMsg = "writing binary ('%s') content to file '%s' " % (mimetype, filepath)
                            logger.warning(warnMsg)

                            with openFile(filepath, "w+b", None) as f:
                                _ = safechardecode(value, True)
                                f.write(_)

                    except Exception as ex:
                        logger.debug(getSafeExString(ex))

                if conf.dumpFormat == DUMP_FORMAT.CSV:
                    if field == fields:
                        dataToDumpFile(dumpFP, "%s" % safeCSValue(value))
                    else:
                        dataToDumpFile(dumpFP, "%s%s" % (safeCSValue(value), conf.csvDel))
                elif conf.dumpFormat == DUMP_FORMAT.HTML:
                    dataToDumpFile(dumpFP, "<td>%s</td>" % getUnicode(htmlEscape(value).encode("ascii", "xmlcharrefreplace")))
                elif conf.dumpFormat == DUMP_FORMAT.JSONL:
                    if len(colValues) <= i or colValues[i] is None or colValues[i] == " ":  # NULL
                        record[safeColumn] = None
                    else:
                        record[safeColumn] = getUnicode(colValues[i])

                field += 1

            if conf.dumpFormat == DUMP_FORMAT.SQLITE:
                try:
                    rtable.insert(values)
                except SqlmapValueException:
                    pass
            elif conf.dumpFormat == DUMP_FORMAT.CSV:
                dataToDumpFile(dumpFP, "\n")
            elif conf.dumpFormat == DUMP_FORMAT.HTML:
                dataToDumpFile(dumpFP, "</tr>\n")
            elif conf.dumpFormat == DUMP_FORMAT.JSONL:
                dataToDumpFile(dumpFP, "%s\n" % getUnicode(json.dumps(record, ensure_ascii=False)))

            self._write("|", console=console)

        self._write("%s\n" % separator)

        if conf.dumpFormat == DUMP_FORMAT.SQLITE:
            rtable.endTransaction()
            logger.info("table '%s.%s' dumped to SQLITE database '%s'" % (db, table, replication.dbpath))

        elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML, DUMP_FORMAT.JSONL):
            if conf.dumpFormat == DUMP_FORMAT.HTML:
                dataToDumpFile(dumpFP, "</tbody>\n</table>\n<script>let lc=-1,ld=1;function sortTable(n,h){var t=document.querySelector(\"table\"),r=Array.from(t.tBodies[0].rows);ld=(lc==n?-ld:1);lc=n;r.sort((a,b)=>{var x=a.cells[n].innerText.trim(),y=b.cells[n].innerText.trim(),nx=parseFloat(x),ny=parseFloat(y);return(!isNaN(nx)&&!isNaN(ny)?(nx-ny)*ld:x.localeCompare(y)*ld)});r.forEach(e=>t.tBodies[0].appendChild(e));Array.from(t.tHead.rows[0].cells).forEach(c=>{c.innerText=c.innerText.replace(/[\u2191\u2193]/g,\"\")});h.innerText=h.innerText+ (ld==1?\"\u2191\":\"\u2193\");}</script>\n</body>\n</html>")
            # Note: each CSV row already ends with '\n' (above); no extra close-newline, otherwise
            # the file ends with a blank line and a later --start/--stop append injects an empty record
            dumpFP.close()

            msg = "table '%s.%s' dumped to %s file '%s'" % (db, table, conf.dumpFormat, dumpFileName)
            if not warnFile:
                logger.info(msg)
            else:
                logger.warning(msg)

    def dbColumns(self, dbColumnsDict, colConsider, dbs):
        self._reportData(dbColumnsDict, CONTENT_TYPE.COLUMNS)

        if conf.api:
            self._write(dbColumnsDict, content_type=CONTENT_TYPE.COLUMNS)

        for column in dbColumnsDict.keys():
            if colConsider == "1":
                colConsiderStr = "s LIKE '%s' were" % unsafeSQLIdentificatorNaming(column)
            else:
                colConsiderStr = " '%s' was" % unsafeSQLIdentificatorNaming(column)

            found = {}
            for db, tblData in dbs.items():
                for tbl, colData in tblData.items():
                    for col, dataType in colData.items():
                        if column.lower() in col.lower():
                            if db in found:
                                if tbl in found[db]:
                                    found[db][tbl][col] = dataType
                                else:
                                    found[db][tbl] = {col: dataType}
                            else:
                                found[db] = {}
                                found[db][tbl] = {col: dataType}

                            continue

            if found:
                msg = "column%s found in the " % colConsiderStr
                msg += "following databases:"
                self._write(msg)

                self.dbTableColumns(found)

    def sqlQuery(self, query, queryRes):
        self.string(query, queryRes, content_type=CONTENT_TYPE.SQL_QUERY)

    def rFile(self, fileData):
        self.lister("files saved to", fileData, sort=False, content_type=CONTENT_TYPE.FILE_READ)

    def registerValue(self, registerData):
        self.string("Registry key value data", registerData, content_type=CONTENT_TYPE.REG_READ, sort=False)

# object to manage how to print the retrieved queries output to
# standard output and sessions file
dumper = Dump()
