diff --git a/lib/core/settings.py b/lib/core/settings.py index de5af0200..d62cc785e 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.130" +VERSION = "1.10.7.131" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) diff --git a/tests/test_decorators.py b/tests/test_decorators.py new file mode 100644 index 000000000..c899c3c94 --- /dev/null +++ b/tests/test_decorators.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +Function decorators in lib/core/decorators.py: cachedmethod (memoization with a +hashable fast path and a frozen slow path for unhashable arguments), stackedmethod +(value-stack realignment) and lockedmethod (reentrant serialization). +""" + +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +from lib.core.decorators import cachedmethod, stackedmethod, lockedmethod +from lib.core.threads import getCurrentThreadData + + +class TestCachedMethod(unittest.TestCase): + def test_memoizes_hashable_args(self): + calls = [] + + @cachedmethod + def f(x): + calls.append(x) + return x * 2 + + self.assertEqual(f(3), 6) + self.assertEqual(f(3), 6) + self.assertEqual(len(calls), 1) # second call served from cache + + def test_memoizes_unhashable_args(self): + calls = [] + + @cachedmethod + def g(seq): + calls.append(1) + return sum(seq) + + self.assertEqual(g([1, 2, 3]), 6) + self.assertEqual(g([1, 2, 3]), 6) # same list content -> cache hit + self.assertEqual(len(calls), 1) + self.assertEqual(g([4, 5]), 9) # different content -> recomputed + self.assertEqual(len(calls), 2) + + def test_tuple_and_list_args_do_not_collide(self): + # regression: a list arg ([1,2],) freezes to ((1,2),), the raw fast key of a tuple + # arg ((1,2),); the two calls must not share a cache slot + @cachedmethod + def kind(x): + return type(x).__name__ + + self.assertEqual(kind((1, 2)), "tuple") + self.assertEqual(kind([1, 2]), "list") + + def test_kwargs_are_part_of_the_key(self): + @cachedmethod + def h(a, b=0): + return a + b + + self.assertEqual(h(1, b=2), 3) + self.assertEqual(h(1, b=5), 6) # different kwarg -> not a cache hit + + +class TestStackedMethod(unittest.TestCase): + def test_realigns_leftover_pushes(self): + td = getCurrentThreadData() + base = len(td.valueStack) + + @stackedmethod + def leaky(_): + td.valueStack.append(_) # pushes without popping + + leaky(1) + self.assertEqual(len(td.valueStack), base) # stack restored to original level + + +class TestLockedMethod(unittest.TestCase): + def test_reentrant(self): + @lockedmethod + def recursive_count(n): + return 0 if n <= 0 else n + recursive_count(n - 1) + + self.assertEqual(recursive_count(5), 15) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_sqlalchemy.py b/tests/test_sqlalchemy.py new file mode 100644 index 000000000..768c12411 --- /dev/null +++ b/tests/test_sqlalchemy.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission + +The SQLAlchemy '-d' connector wrapper (lib/utils/sqlalchemy.py). The absolute +SQLite path must map to 'sqlite:///' + abspath: an extra slash yields the db +'//path' (tolerated only on Linux by accident) and, on Windows, a broken +'/C:\\...' that fails to open. +""" + +import os +import sqlite3 +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from _testutils import bootstrap +bootstrap() + +try: + import sqlalchemy as _sa + _HAVE_SA = hasattr(_sa, "dialects") +except ImportError: + _HAVE_SA = False + +from lib.core.data import conf +from lib.utils.sqlalchemy import SQLAlchemy + + +@unittest.skipUnless(_HAVE_SA, "SQLAlchemy not installed") +class TestSQLAlchemySqlitePath(unittest.TestCase): + _KEYS = ("direct", "dbmsUser", "dbmsPass", "hostname", "port", "dbmsDb") + + def setUp(self): + self._saved = dict((k, conf.get(k)) for k in self._KEYS) + + def tearDown(self): + for k, v in self._saved.items(): + conf[k] = v + + def test_absolute_sqlite_path_opens_correct_file(self): + d = tempfile.mkdtemp(prefix="sqlmap-sa-test") + dbfile = os.path.join(d, "target.db") + con = sqlite3.connect(dbfile) + con.execute("CREATE TABLE t (x TEXT)") + con.execute("INSERT INTO t VALUES ('secret')") + con.commit() + con.close() + + conf.direct = "sqlite://%s" % dbfile + conf.dbmsUser = conf.dbmsPass = None + conf.hostname = None + conf.port = None + conf.dbmsDb = dbfile + + sa = SQLAlchemy(dialect="sqlite") + sa.connect() + + # the reformatted URL must resolve to the exact absolute file (not '//...path') + self.assertEqual(_sa.engine.url.make_url(sa.address).database, os.path.abspath(dbfile)) + # and end-to-end it must read from that file + self.assertEqual(sa.select("SELECT x FROM t"), [("secret",)]) + + +if __name__ == "__main__": + unittest.main(verbosity=2)