"""Safe, read-only Firebird/SGV discovery for Setup.exe. Finds candidate .FDB files, and for each one checks (read-only connection, no writes, ever) that it is an SGV database: it must contain both C000025 (products) and C000004 (company). Exactly one compatible database is selected automatically; several are NEVER chosen silently — the caller (wizard) asks the operator during installation. Credentials are only ever used locally. Pure/injectable on purpose (`connect`, `finder`) so it is unit-testable without Firebird or a Windows host. """ from __future__ import annotations import os import subprocess from dataclasses import dataclass, field from pathlib import Path from typing import Callable # The SGV/Firebird default. Tried silently; anything else is asked for inside the wizard. DEFAULT_CREDENTIALS: tuple[tuple[str, str], ...] = (("SYSDBA", "masterkey"),) REQUIRED_TABLES = ("C000025", "C000004") MAX_DEPTH = 4 MIN_BYTES = 200 * 1024 MAX_FILES = 300 SKIP_DIRS = {"windows", "program files", "program files (x86)", "programdata", "$recycle.bin", "system volume information", "appdata", "node_modules", ".git", "winsxs", "recovery", "perflogs", "users"} SKIP_FILES = {"security2.fdb", "security3.fdb", "security4.fdb", "security.fdb"} @dataclass class Candidate: path: str status: str # OK | NOT_SGV | AUTH_FAILED | ERROR user: str | None = None password: str | None = None detail: str = "" @dataclass class Discovery: state: str # SELECTED | AMBIGUOUS | AUTH_REQUIRED | NONE compatible: list[Candidate] = field(default_factory=list) auth_failed: list[Candidate] = field(default_factory=list) scanned: int = 0 firebird_service: str | None = None @property def selected(self) -> Candidate | None: return self.compatible[0] if self.state == "SELECTED" else None def fixed_drives() -> list[str]: try: import ctypes mask = ctypes.windll.kernel32.GetLogicalDrives() drives = [] for i in range(26): root = f"{chr(65 + i)}:\\" if mask & (1 << i) and ctypes.windll.kernel32.GetDriveTypeW(root) == 3: # DRIVE_FIXED drives.append(root) return drives except Exception: # noqa: BLE001 return ["C:\\"] def find_fdb_files(roots: list[str], max_depth: int = MAX_DEPTH, limit: int = MAX_FILES) -> list[str]: found: list[str] = [] for root in roots: root = str(root) base_depth = root.rstrip("\\/").count(os.sep) for current, dirs, files in os.walk(root, onerror=lambda _e: None): depth = current.rstrip("\\/").count(os.sep) - base_depth dirs[:] = [d for d in dirs if d.lower() not in SKIP_DIRS] if depth < max_depth else [] for name in files: lower = name.lower() if lower.endswith(".fdb") and lower not in SKIP_FILES: full = os.path.join(current, name) try: if os.path.getsize(full) >= MIN_BYTES: found.append(full) except OSError: continue if len(found) >= limit: return found return found def default_roots() -> list[str]: roots: list[str] = [] for var in ("ProgramFiles", "ProgramFiles(x86)"): base = os.environ.get(var) if base: roots.extend(str(p) for p in Path(base).glob("Firebird*")) roots.extend(fixed_drives()) return roots def firebird_service() -> str | None: try: out = subprocess.run(["sc.exe", "query", "type=", "service", "state=", "all"], capture_output=True, text=True, timeout=20).stdout except Exception: # noqa: BLE001 return None for line in out.splitlines(): if line.strip().upper().startswith("SERVICE_NAME") and "FIREBIRD" in line.upper(): return line.split(":", 1)[1].strip() return None def _default_connect(path: str, user: str, password: str): import fdb return fdb.connect(database=path, user=user, password=password, isolation_level=fdb.ISOLATION_LEVEL_READ_COMMITED_RO) def probe(path: str, user: str, password: str, *, connect: Callable | None = None) -> Candidate: connect = connect or _default_connect try: conn = connect(path, user, password) except Exception as error: # noqa: BLE001 text = str(error).lower() if "user name" in text or "password" in text or "not defined" in text: return Candidate(path, "AUTH_FAILED", detail="credenciais recusadas") return Candidate(path, "ERROR", detail=type(error).__name__) try: cursor = conn.cursor() cursor.execute("SELECT TRIM(RDB$RELATION_NAME) FROM RDB$RELATIONS WHERE RDB$RELATION_NAME IN ('C000025','C000004')") have = {row[0] for row in cursor.fetchall()} except Exception as error: # noqa: BLE001 return Candidate(path, "ERROR", detail=type(error).__name__) finally: try: conn.close() except Exception: # noqa: BLE001 pass if all(table in have for table in REQUIRED_TABLES): return Candidate(path, "OK", user, password) return Candidate(path, "NOT_SGV", detail="sem C000025/C000004") def discover(*, roots: list[str] | None = None, credentials=DEFAULT_CREDENTIALS, connect: Callable | None = None, finder: Callable | None = None) -> Discovery: finder = finder or find_fdb_files files = finder(roots if roots is not None else default_roots()) compatible: list[Candidate] = [] auth_failed: list[Candidate] = [] for path in files: result = None for user, password in credentials: result = probe(path, user, password, connect=connect) if result.status != "AUTH_FAILED": break if result is None: continue if result.status == "OK": compatible.append(result) elif result.status == "AUTH_FAILED": auth_failed.append(result) if len(compatible) == 1: state = "SELECTED" elif len(compatible) > 1: state = "AMBIGUOUS" elif auth_failed: state = "AUTH_REQUIRED" else: state = "NONE" return Discovery(state, compatible, auth_failed, len(files), firebird_service())