272 lines
12 KiB
Plaintext
272 lines
12 KiB
Plaintext
from stdint import *
|
|
import stdlib
|
|
import string
|
|
import memhub
|
|
import lib.VFs as VFs
|
|
import lib.VLog as VLog
|
|
import win32.win32base as wb
|
|
|
|
|
|
# ============================================================
|
|
# VDirs - common directory table (auto-built / forced override via --path)
|
|
#
|
|
# Without --path: keep the auto table (two static entries + per-drive Program Files + desktop).
|
|
# With --path: no auto-building; table = the command-line paths (ordered, de-duplicated).
|
|
# Other modules (VPe/VScan) only read via DirCount()/DirAt(i).
|
|
#
|
|
# Win32 self-declared (no repo declaration; x64 calling convention):
|
|
# kernel32!GetLogicalDrives / GetDriveTypeA / GetEnvironmentVariableA / SetErrorMode;
|
|
# shell32!SHGetKnownFolderPath; ole32!CoTaskMemFree.
|
|
# Declared per Win32 API docs (winbase.h / shlobj_core.h / combaseapi.h).
|
|
# ============================================================
|
|
|
|
# Win32: typedef LONG HRESULT (signed 32-bit; negative = failure)
|
|
typedef HRESULT = int32
|
|
# Win32: typedef const KNOWNFOLDERID *REFKNOWNFOLDERID (points to a 16-byte GUID)
|
|
typedef REFKNOWNFOLDERID = rawptr
|
|
|
|
extern def GetLogicalDrives() -> DWORD
|
|
extern def GetDriveTypeA(lpRootPathName: wb.LPCSTR) -> UINT
|
|
extern def GetEnvironmentVariableA(lpName: wb.LPCSTR, lpBuffer: CHARPTR, nSize: DWORD) -> DWORD
|
|
extern def SetErrorMode(uMode: UINT) -> UINT
|
|
extern def SHGetKnownFolderPath(rfid: REFKNOWNFOLDERID, dwFlags: DWORD, hToken: wb.HANDLE,
|
|
ppszPath: WCHARPTR*) -> HRESULT
|
|
extern def CoTaskMemFree(pv: rawptr)
|
|
|
|
define SEM_FAILCRITICALERRORS = 0x0001 # SetErrorMode flag (errhandlingapi.h)
|
|
define KNOWNFOLDERID_SIZE = 16 # KNOWNFOLDERID / GUID byte size
|
|
# winnt.h: FILE_ATTRIBUTE_REPARSE_POINT is used by VScan
|
|
|
|
# GetDriveTypeA return values (winbase.h)
|
|
define DRIVE_NO_ROOT_DIR = 1 # skip: no root path
|
|
define DRIVE_REMOVABLE = 2 # probe: removable media (USB/card reader)
|
|
define DRIVE_FIXED = 3 # probe: fixed disk
|
|
define DRIVE_REMOTE = 4 # probe: network drive
|
|
define DRIVE_CDROM = 5 # skip: CD-ROM
|
|
# Only NO_ROOT_DIR(1) and CDROM(5) are skipped; all others (incl. REMOVABLE) probe Program Files.
|
|
# SetErrorMode in AddFixedDriveProgramFiles suppresses popups/blocking from empty media.
|
|
|
|
define COMMON_DIR_CAPACITY = 128 # table capacity (inline array element count)
|
|
define DRIVE_ROOT_CAP = 8 # "X:\" + NUL buffer
|
|
define DESKTOP_SUFFIX_CAP = 9 # r"\Desktop"(8) + NUL(1)
|
|
|
|
# Injected by App/main.vp
|
|
_mbuddy: memhub.MemBuddy*
|
|
|
|
# Explicit-length inline array + count: avoids t.CArray[str] single-arg form differing
|
|
# between TPC (auto-length inline array) and ViperC (pointer).
|
|
Common_Directory: [COMMON_DIR_CAPACITY]str
|
|
Common_Dir_Count: int = 0 # used slot count
|
|
|
|
# --path overrides (ordered; collected at parse time, existence checked and warned at build)
|
|
Override_Dir: [COMMON_DIR_CAPACITY]str
|
|
Override_Mode: int = 0 # 1 = --path seen (force replace)
|
|
Override_Count: int = 0
|
|
|
|
|
|
# Table read API (other modules read only through this, never touch Common_Directory)
|
|
def DirCount() -> int:
|
|
return Common_Dir_Count
|
|
|
|
|
|
def DirAt(Idx: int) -> str:
|
|
return Common_Directory[Idx]
|
|
|
|
|
|
def IsOverride() -> int:
|
|
return Override_Mode
|
|
|
|
|
|
# Called on first --path (idempotent: clears overrides once); no auto-build afterwards
|
|
def BeginOverride():
|
|
global Override_Mode, Override_Count
|
|
if Override_Mode == 0:
|
|
Override_Mode = 1
|
|
Override_Count = 0
|
|
|
|
|
|
# Collect one --path override (ordered; existence check deferred to BuildTable)
|
|
def AddOverridePath(Path: str) -> int:
|
|
global Override_Count
|
|
if Path is None:
|
|
return 0
|
|
if Override_Count >= COMMON_DIR_CAPACITY:
|
|
VLog.LogFmtIS(" Warning: --path count reached limit (%d), ignored: %s\n", COMMON_DIR_CAPACITY, Path)
|
|
return 0
|
|
Override_Dir[Override_Count] = Path
|
|
Override_Count += 1
|
|
return 1
|
|
|
|
|
|
# Unconditional insert: strcmp de-dup + capacity bound only, no existence check. 1=added, 0=skipped.
|
|
# Path must live for the whole process (static literal or MemBuddy pool allocation).
|
|
def AddCommonDirForce(Path: str) -> int:
|
|
global Common_Dir_Count
|
|
if Path is None:
|
|
return 0
|
|
for i in range(Common_Dir_Count):
|
|
if string.strcmp(Common_Directory[i], Path) == 0:
|
|
return 0
|
|
if Common_Dir_Count >= COMMON_DIR_CAPACITY:
|
|
VLog.LogFmt1(" Warning: common dir table full (%d), entry ignored\n", sizet(COMMON_DIR_CAPACITY))
|
|
return 0
|
|
Common_Directory[Common_Dir_Count] = Path
|
|
Common_Dir_Count += 1
|
|
return 1
|
|
|
|
|
|
# Add to the common dir table: existence check + de-dup + capacity bound (scanned/derived paths).
|
|
# Returns 1=added, 0=skipped (missing / duplicate / table full)
|
|
def AddCommonDir(Path: str) -> int:
|
|
if Path is None:
|
|
return 0
|
|
if VFs.PathIsDir(Path) == 0:
|
|
return 0
|
|
return AddCommonDirForce(Path)
|
|
|
|
|
|
# Enumerate X:\Program Files and X:\Program Files (x86) on each drive; returns added count
|
|
def AddFixedDriveProgramFiles() -> int:
|
|
# Process-level: suppress critical-error dialogs and make empty drives fail immediately,
|
|
# avoiding long blocks. Affects all later file ops for the process lifetime (not auto-reset).
|
|
SetErrorMode(SEM_FAILCRITICALERRORS)
|
|
Mask: DWORD = GetLogicalDrives()
|
|
if Mask == 0:
|
|
VLog.LogFmt1(" Warning: GetLogicalDrives failed (win32 err=%u)\n", wb.GetLastError())
|
|
return 0
|
|
Added: int = 0
|
|
for Letter in range(26):
|
|
if (Mask & (uint32(1) << uint32(Letter))) != 0:
|
|
Root: bytes = _mbuddy.alloc(DRIVE_ROOT_CAP)
|
|
if Root is not None:
|
|
Root[0] = char(65 + Letter) # 'A' + Letter
|
|
Root[1] = ':'
|
|
Root[2] = '\\'
|
|
Root[3] = 0
|
|
Dt: UINT = GetDriveTypeA(Root)
|
|
# Only NO_ROOT_DIR(1) and CDROM(5) are skipped; REMOVABLE(2)/FIXED(3)/
|
|
# REMOTE(4) still probe Program Files (empty media handled by SetErrorMode above).
|
|
if Dt != DRIVE_NO_ROOT_DIR and Dt != DRIVE_CDROM:
|
|
Pf: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
|
|
if Pf is not None:
|
|
string.strcpy(Pf, r"Q:\Program Files") # 'Q' placeholder, rewritten to the drive letter next line
|
|
Pf[0] = char(65 + Letter)
|
|
Added += AddCommonDir(str(Pf))
|
|
Pfx: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
|
|
if Pfx is not None:
|
|
string.strcpy(Pfx, r"Q:\Program Files (x86)")
|
|
Pfx[0] = char(65 + Letter)
|
|
Added += AddCommonDir(str(Pfx))
|
|
return Added
|
|
|
|
|
|
# Desktop fallback: %USERPROFILE%\Desktop (only when SHGetKnownFolderPath is unavailable/fails).
|
|
# Buf is caller-provided (>= PATH_BUF_CAP). Returns 1=added, 0=not added
|
|
def AddDesktopFallback(Buf: bytes) -> int:
|
|
n: DWORD = GetEnvironmentVariableA("USERPROFILE", Buf, VFs.PATH_BUF_CAP)
|
|
# Also append "\Desktop" (8 bytes) + NUL, so n + 9 must not exceed the buffer capacity
|
|
if n == 0 or n + uint32(DESKTOP_SUFFIX_CAP) > uint32(VFs.PATH_BUF_CAP):
|
|
VLog.LogFmt1(" desktop fallback: USERPROFILE missing or too long (win32 err=%u)\n", wb.GetLastError())
|
|
return 0
|
|
Buf[n] = 0
|
|
string.strcat(Buf, r"\Desktop")
|
|
VLog.VerboseFmtS1(" desktop fallback: using %s\n", str(Buf))
|
|
if AddCommonDir(str(Buf)) != 0:
|
|
return 1
|
|
VLog.LogStr(" Warning: desktop fallback path missing or not a directory, not added to common dir table\n")
|
|
return 0
|
|
|
|
|
|
# Current user desktop: prefer SHGetKnownFolderPath(FOLDERID_Desktop) (handles OneDrive
|
|
# redirection), else fall back explicitly to %USERPROFILE%\Desktop. Returns 1=added, 0=not added
|
|
def AddCurrentUserDesktop() -> int:
|
|
Buf: bytes = _mbuddy.alloc(VFs.PATH_BUF_CAP)
|
|
if Buf is None:
|
|
VLog.LogStr(" Warning: desktop path buffer allocation failed\n")
|
|
return 0
|
|
|
|
# FOLDERID_Desktop {B4BFCC3A-DB2C-424C-B029-7FE99A87C641} as 16 raw LE bytes:
|
|
# 3A CC BF B4 2C DB 4C 42 B0 29 7F E9 9A 87 C6 41 (wb.GUID mis-models Data4 as a pointer).
|
|
GuidBuf: BYTEPTR = stdlib.malloc(KNOWNFOLDERID_SIZE)
|
|
if GuidBuf is None:
|
|
VLog.LogStr(" Warning: KNOWNFOLDERID buffer allocation failed, using fallback path\n")
|
|
return AddDesktopFallback(Buf)
|
|
GuidBuf[0] = 0x3A
|
|
GuidBuf[1] = 0xCC
|
|
GuidBuf[2] = 0xBF
|
|
GuidBuf[3] = 0xB4
|
|
GuidBuf[4] = 0x2C
|
|
GuidBuf[5] = 0xDB
|
|
GuidBuf[6] = 0x4C
|
|
GuidBuf[7] = 0x42
|
|
GuidBuf[8] = 0xB0
|
|
GuidBuf[9] = 0x29
|
|
GuidBuf[10] = 0x7F
|
|
GuidBuf[11] = 0xE9
|
|
GuidBuf[12] = 0x9A
|
|
GuidBuf[13] = 0x87
|
|
GuidBuf[14] = 0xC6
|
|
GuidBuf[15] = 0x41
|
|
|
|
Pwsz: WCHARPTR = None
|
|
Hr: HRESULT = SHGetKnownFolderPath(rawptr(GuidBuf), 0, None, &Pwsz)
|
|
stdlib.free(rawptr(GuidBuf))
|
|
if Hr < 0:
|
|
# On failure *ppszPath is NULL by spec; still free a non-NULL return defensively to avoid leaks
|
|
if Pwsz is not None:
|
|
CoTaskMemFree(rawptr(Pwsz))
|
|
VLog.LogFmt1(" desktop: SHGetKnownFolderPath failed (hr=0x%X), using fallback path\n", sizet(uint32(Hr)))
|
|
return AddDesktopFallback(Buf)
|
|
if Pwsz is None:
|
|
VLog.LogStr(" desktop: SHGetKnownFolderPath returned null pointer, using fallback path\n")
|
|
return AddDesktopFallback(Buf)
|
|
|
|
# UTF-16 -> UTF-8: query needed bytes first (incl. NUL; Needed<=0 means failure)
|
|
Needed: int = VFs.WideToUtf8Needed(Pwsz)
|
|
if Needed <= 0 or Needed > VFs.PATH_BUF_CAP:
|
|
CoTaskMemFree(rawptr(Pwsz))
|
|
VLog.LogFmt2(" desktop: path empty or too long (%d bytes, limit %d), using fallback path\n",
|
|
sizet(uint32(Needed)), sizet(VFs.PATH_BUF_CAP))
|
|
return AddDesktopFallback(Buf)
|
|
Got: int = VFs.WideToUtf8(Pwsz, Buf, VFs.PATH_BUF_CAP)
|
|
CoTaskMemFree(rawptr(Pwsz)) # Free immediately after use: single free point for both branches
|
|
if Got == 0:
|
|
VLog.LogFmt1(" desktop: UTF-16 to UTF-8 conversion failed (win32 err=%u), using fallback path\n", wb.GetLastError())
|
|
return AddDesktopFallback(Buf)
|
|
|
|
if AddCommonDir(str(Buf)) != 0:
|
|
return 1
|
|
VLog.LogStr(" Warning: desktop path missing or not a directory, not added to common dir table\n")
|
|
return 0
|
|
|
|
|
|
# Print the common dir table (verbose only). Returns 0=non-empty, 1=empty
|
|
def PrintCommonDirs() -> int:
|
|
if Common_Dir_Count <= 0:
|
|
VLog.VerboseStr(" (table empty)\n")
|
|
return 1
|
|
for i in range(Common_Dir_Count):
|
|
VLog.VerboseFmtIS(" [%d] %s\n", sizet(i), Common_Directory[i])
|
|
VLog.VerboseFmt1(" total %d common dirs\n", sizet(Common_Dir_Count))
|
|
return 0
|
|
|
|
|
|
# Build and print the dir table (call after _mbuddy injection). Returns 0=non-empty, 1=empty.
|
|
# Logs distinguish "auto table" / "override table (N)"; missing --paths are warned and skipped.
|
|
def BuildTable() -> int:
|
|
if Override_Mode == 0:
|
|
AddCommonDirForce(r"C:\Viper") # Two static entries: unconditional insert (no existence check)
|
|
AddCommonDirForce(r"D:\Viper")
|
|
AddFixedDriveProgramFiles() # Per-drive Program Files / Program Files (x86)
|
|
AddCurrentUserDesktop() # Current user desktop
|
|
VLog.VerboseFmt1(" dir table source: auto table (%u entries)\n", sizet(Common_Dir_Count))
|
|
else:
|
|
# Force replace: no auto-build; table = --path order (de-dup handled by AddCommonDirForce)
|
|
for i in range(Override_Count):
|
|
if VFs.PathIsDir(Override_Dir[i]) == 0:
|
|
VLog.LogFmtS1(" Warning: --path missing or not a directory, skipped: %s\n", Override_Dir[i])
|
|
else:
|
|
AddCommonDirForce(Override_Dir[i])
|
|
VLog.VerboseFmt1(" dir table source: override table (%u entries)\n", sizet(Common_Dir_Count))
|
|
return PrintCommonDirs()
|