# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

# Don't try to optimize this code; I tried but it's entirely I/O bound

import os
import hashlib
import sys
import stat
import time
import logging

logger = logging.getLogger("gather.fswalker")

class FileSystemWalker(object):

    def __init__(self, collector, skips=None):
        columns = """code md5
                     st_mode st_ino st_dev st_nlink st_uid
                     st_gid st_size st_atime st_mtime st_ctime
                     filename linkto error""".split()
        self.table = collector.table("md5", columns)
        if skips is None:
            skips = []
        self.ncskips = [os.path.normcase(s) for s in skips]
        self.lcskips = [s.lower() for s in skips]

    _throttle = 0

    def throttle(self, nbytes=1000):
        self._throttle += nbytes
        if self._throttle >= 500000:
            time.sleep(0.1)
            self._throttle = 0

    def walk(self, path):
        ##logger.debug("path = %r", path)
        self.throttle()
        if path.lower() in self.lcskips:
            if os.path.normcase(path) in self.ncskips:
                logger.info("Skipping %r", path)
                return
        try:
            st = os.lstat(path)
        except os.error as err:
            self.printerr(err, path)
        else:
            if stat.S_ISREG(st.st_mode):
                self.printmd5(st, path)
            elif stat.S_ISDIR(st.st_mode):
                filenames = []
                err = ""
                try:
                    filenames = os.listdir(path)
                except os.error as err:
                    pass
                self.write("DIR", "", st, path, err)
                filenames.sort()
                for fn in filenames:
                    self.walk(os.path.join(path, fn))
            elif stat.S_ISLNK(st.st_mode):
                try:
                    linkto = os.readlink(path)
                except os.error as err:
                    linkto = ""
                self.write("LINK", "", st, path, linkto=linkto)
            else:
                self.write("STAT", "", st, path)

    def printerr(self, err, fn):
        self.write("ERR", "", "", fn, err)

    def printmd5(self, st, fn):
        try:
            f = open(fn, "rb")
        except (OSError, IOError) as err:
            self.write("OPEN", "", st, fn, err)
        else:
            try:
                try:
                    sum = self.calcmd5(f)
                except (OSError, IOError) as err:
                    self.write("READ", "", st, fn, err)
                else:
                    self.write("FILE", sum, st, fn)
            finally:
                f.close()

    def calcmd5(self, f):
        t0 = time.time()
        # If calculating an MD5 checksum takes more than 5 minutes,
        # something's wrong and we stop.
        stoptime = t0 + 300
        BUFSIZE = 8*1024
        summer = hashlib.md5()
        nbytes = 0
        while True:
            block = f.read(BUFSIZE)
            if not block:
                break
            if isinstance(block, str):
                block = block.encode('utf-8')
            summer.update(block)
            n = len(block)
            nbytes += n
            self.throttle(n)
            t1 = time.time()
            if t1 > stoptime:
                logger.warning("Time-out in MD5 calculation after %d bytes"
                               " for %r", nbytes, f.name)
                raise IOError("Time-out in MD5 calculation after %d bytes" %
                              nbytes)
        return summer.hexdigest()

    def write(self, code, md5, st, fn, err="", linkto=""):
        if st:
            st = tuple(st)
        else:
            st = ("",) * 10
        self.table.add((code, md5) + st + (fn, linkto, err))

def test():
    from sigma.gather import gathering
    c = gathering.TestCollector()
    fsw = FileSystemWalker(c)
    fsw.walk(".")

if __name__ == "__main__":
    test()
