#!/usr/bin/env python

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

Parsers under lib/parse/: DBMS banner fingerprinting (banner.py + the shared
FingerprintHandler in handler.py) and the .ini configuration-file reader
(configfile.py). These are pure: given a banner string (and the shipped XML
signature files) or a config file on disk, they populate kb/conf with no
network or DBMS. We drive each over realistic inputs and assert the extracted
fingerprint / parsed options.
"""

import os
import sys
import tempfile
import unittest

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap, set_dbms, reset_dbms
bootstrap()

from lib.core.data import kb, conf
from lib.core.enums import DBMS
from lib.parse.banner import bannerParser, MSSQLBannerHandler
from lib.parse.handler import FingerprintHandler


class TestFingerprintHandler(unittest.TestCase):
    def test_feedinfo_dbms_version_scalar(self):
        info = {}
        h = FingerprintHandler("some banner", info)
        h._feedInfo("dbmsVersion", "5.7.1")
        self.assertEqual(info["dbmsVersion"], "5.7.1")

    def test_feedinfo_set_valued_keys_split_on_pipe(self):
        info = {}
        h = FingerprintHandler("some banner", info)
        h._feedInfo("type", "Linux|Debian")
        self.assertIsInstance(info["type"], set)
        self.assertEqual(info["type"], set(["Linux", "Debian"]))

    def test_feedinfo_ignores_empty_and_none(self):
        info = {}
        h = FingerprintHandler("b", info)
        h._feedInfo("type", "")
        h._feedInfo("type", "None")
        h._feedInfo("type", None)
        self.assertNotIn("type", info)


class TestBannerParser(unittest.TestCase):
    def setUp(self):
        self._saved = kb.bannerFp
        kb.bannerFp = {}

    def tearDown(self):
        kb.bannerFp = self._saved

    def test_no_dbms_is_noop(self):
        # without an identified DBMS bannerParser must bail out before touching kb.bannerFp
        from lib.core.common import Backend
        Backend.flushForcedDbms(force=True)
        saved = (conf.get("forceDbms"), kb.get("dbms"))
        conf.forceDbms = None
        kb.dbms = None
        try:
            kb.bannerFp = {}
            self.assertIsNone(bannerParser("PostgreSQL 9.5.3 on x86_64-pc-linux-gnu"))
            # no back-end identified -> the early return leaves the fingerprint untouched
            self.assertEqual(kb.bannerFp, {})
        finally:
            conf.forceDbms, kb.dbms = saved

    def test_mysql_banner_populates_version(self):
        set_dbms(DBMS.MYSQL)
        kb.bannerFp = {}
        bannerParser("5.0.51a-3ubuntu5.4")
        # the generic signatures classify the OS/distrib from the banner tail
        self.assertTrue(kb.bannerFp, msg="no fingerprint extracted")
        self.assertIn("Ubuntu", kb.bannerFp.get("distrib", set()))

    def test_oracle_banner_populates_version(self):
        set_dbms(DBMS.ORACLE)
        kb.bannerFp = {}
        bannerParser("Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production")
        self.assertIn("dbmsVersion", kb.bannerFp)
        self.assertTrue(kb.bannerFp["dbmsVersion"].startswith("11.2.0"))

    def test_pgsql_banner_populates_version(self):
        set_dbms(DBMS.PGSQL)
        kb.bannerFp = {}
        # the shipped PostgreSQL signature 'PostgreSQL\s+([\w\.]+)' captures the version
        bannerParser("PostgreSQL 9.5.3 on x86_64-pc-linux-gnu")
        self.assertIn("dbmsVersion", kb.bannerFp)
        self.assertEqual(kb.bannerFp["dbmsVersion"], "9.5.3")

    def test_mssql_banner_populates_release_and_version(self):
        set_dbms(DBMS.MSSQL)
        kb.bannerFp = {}
        # a real SQL Server 2008 RTM build present in data/xml/banner/mssql.xml,
        # so the MSSQLBannerHandler resolves both the release year and the version
        bannerParser("Microsoft SQL Server 2008 - 10.00.4311.00")
        self.assertEqual(kb.bannerFp.get("dbmsRelease"), "2008")
        self.assertEqual(kb.bannerFp.get("dbmsVersion"), "10.00.4311")


class TestMSSQLBannerHandler(unittest.TestCase):
    def test_version_alt_built_for_dotzero_form(self):
        info = {}
        h = MSSQLBannerHandler("Microsoft SQL Server 10.00.1600.22", info)
        h.startElement("version", {})
        h.characters("10.00.1600")
        h.endElement("version")
        # endElement('version') derives the "<major>.0.<build>.0" alternate form
        self.assertEqual(h._versionAlt, "10.0.1600.0")


class _Attrs(dict):
    """Minimal SAX-attrs stand-in (supports .get)."""


class TestMSSQLBannerHandlerServicePack(unittest.TestCase):
    def test_servicepack_strips_spaces(self):
        info = {}
        h = MSSQLBannerHandler("banner", info)
        h.startElement("servicepack", {})
        h.characters(" 2 ")
        h.endElement("servicepack")
        self.assertEqual(h._servicePack, "2")


class TestConfigFileParser(unittest.TestCase):
    def _write_cfg(self, body):
        fd, path = tempfile.mkstemp(suffix=".ini", prefix="sqlmapcfg_")
        os.close(fd)
        with open(path, "w") as f:
            f.write(body)
        return path

    def test_parses_target_and_typed_options(self):
        from lib.parse.configfile import configFileParser
        path = self._write_cfg(
            "[Target]\n"
            "url = http://config.invalid/?id=1\n"
            "[Optimization]\n"
            "threads = 4\n"
            "[Injection]\n"
            "tamper = space2comment\n"
        )
        saved = {k: conf.get(k) for k in ("url", "threads", "tamper")}
        try:
            configFileParser(path)
            self.assertEqual(conf.url, "http://config.invalid/?id=1")
            self.assertEqual(conf.threads, 4)            # INTEGER datatype coerced
            self.assertEqual(conf.tamper, "space2comment")
        finally:
            for k, v in saved.items():
                conf[k] = v
            os.remove(path)

    def test_missing_target_section_raises(self):
        from lib.parse.configfile import configFileParser
        from lib.core.exception import SqlmapMissingMandatoryOptionException
        path = self._write_cfg("[Request]\nthreads = 1\n")
        try:
            self.assertRaises(SqlmapMissingMandatoryOptionException,
                              configFileParser, path)
        finally:
            os.remove(path)


if __name__ == "__main__":
    unittest.main()


def tearDownModule():
    reset_dbms()   # clear any DBMS forced via set_dbms() so it can't leak into later test modules
