#!/usr/bin/env python

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

Secure directory purge (lib/utils/purge.py, the --purge feature): multi-pass
overwrite + truncation + removal of a directory's content. Driven against a
throwaway temp tree so the real output dir is never touched.
"""

import os
import shutil
import sys
import tempfile
import unittest

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

import lib.utils.purge as purge_mod
from lib.utils.purge import purge


class _RecordingLogger(object):
    """Captures every (level, message) emitted while installed as purge.logger."""

    def __init__(self):
        self.records = []

    def _add(self, level):
        return lambda msg, *a: self.records.append((level, msg % a if a else msg))

    def __getattr__(self, name):
        if name in ("warning", "info", "debug", "error", "critical"):
            return self._add(name)
        raise AttributeError(name)

    def messages(self, level=None):
        return [m for (lvl, m) in self.records if level is None or lvl == level]


class TestPurge(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.mkdtemp(prefix="sqlmap_purge_")

    def tearDown(self):
        shutil.rmtree(self.tmp, ignore_errors=True)

    def test_overwrites_and_truncates_file_contents(self):
        # a couple of files + a nested subdir with a (non-empty) file
        plaintexts = {
            os.path.join(self.tmp, "a.txt"): "secret data",
            os.path.join(self.tmp, "sub", "b.txt"): "more secret data",
        }
        with open(os.path.join(self.tmp, "a.txt"), "w") as f:
            f.write(plaintexts[os.path.join(self.tmp, "a.txt")])
        with open(os.path.join(self.tmp, "empty.bin"), "w") as f:
            pass
        os.mkdir(os.path.join(self.tmp, "sub"))
        with open(os.path.join(self.tmp, "sub", "b.txt"), "w") as f:
            f.write(plaintexts[os.path.join(self.tmp, "sub", "b.txt")])

        # neutralise the final rmtree so the overwrite/truncate work product remains
        # observable on disk; the files are renamed, so locate them by walking the tree.
        real_rmtree = purge_mod.shutil.rmtree
        purge_mod.shutil.rmtree = lambda *a, **k: None
        try:
            purge(self.tmp)
        finally:
            purge_mod.shutil.rmtree = real_rmtree

        # collect every surviving regular file (names are randomised by purge)
        survivors = []
        for root, _dirs, files in os.walk(self.tmp):
            for name in files:
                survivors.append(os.path.join(root, name))

        # the originally non-empty files still exist (rmtree was a no-op) but the
        # multi-pass overwrite + truncation reduced each to size 0 and the original
        # plaintext is gone.
        nonempty = [p for p in survivors if os.path.getsize(p) > 0]
        self.assertEqual(nonempty, [], msg="files were not truncated to zero: %r" % nonempty)

        blob = b""
        for p in survivors:
            with open(p, "rb") as fh:
                blob += fh.read()
        for secret in plaintexts.values():
            self.assertNotIn(secret.encode("utf-8"), blob,
                             msg="original plaintext %r survived the purge" % secret)

    def test_purges_nested_content(self):
        # full purge (including rmtree) wipes the whole tree
        with open(os.path.join(self.tmp, "a.txt"), "w") as f:
            f.write("secret data")
        sub = os.path.join(self.tmp, "sub")
        os.mkdir(sub)
        with open(os.path.join(sub, "b.txt"), "w") as f:
            f.write("more secret data")

        purge(self.tmp)

        self.assertFalse(os.path.exists(self.tmp))

    def test_nonexistent_directory_is_noop(self):
        missing = os.path.join(self.tmp, "does_not_exist")

        real_logger = purge_mod.logger
        rec = _RecordingLogger()
        purge_mod.logger = rec
        try:
            # must not raise; the guard branch logs a skip warning and returns
            purge(missing)
        finally:
            purge_mod.logger = real_logger

        self.assertFalse(os.path.exists(missing))
        self.assertTrue(
            any("skipping purging" in w and "does not exist" in w for w in rec.messages("warning")),
            msg="nonexistent-directory guard did not log its warning: %r" % rec.records,
        )


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