from stdint import * import stdlib import string import memhub import json import lib.VFs as VFs import lib.VLog as VLog import win32.win32file as wf import win32.win32base as wb # ============================================================ # VCache - discovery cache (in-memory table + cache file read/write/parse/validation) # # Decoupled from VJson: WriteCacheFile takes (Buf, Len) from the caller instead of reading # VJson's buffer; entries are exposed via CacheCount()/CacheExe(i)... to avoid an import cycle. # # Verbosity: load/per-entry-removal detail goes through VLog.Verbose*; parse/read fallback # messages (text too long, pool precheck short, JSON parse failure, write failure) always print. # # Parsing uses the repo json library json.parse, then copies fields into the C-side table. # Memory ownership is documented at ParseCacheText. # ============================================================ define CACHE_CAP = 256 # cache entry limit define CACHE_FILE_MAX = 16777216 # cache file read limit 16 MiB define DISP_CAP = 160 # "Viper " + version string define CACHE_TEXT_MAX = 262144 # parse text limit 256 KiB (typical ~60 KiB; larger = corrupt, start empty) define CACHE_ARENA_PER_BYTE = 128 # parse working pool capacity per input byte (upper bound, see ParseCacheText) define CACHE_ARENA_SLACK = 4096 # parse working pool fixed slack # Injected by App/main.vp _mbuddy: memhub.MemBuddy* # Discovery cache (in-memory; ANSI paths, directly comparable to FindFirstFileA results). # Cache_Src holds the entry source ("workspace"=scan found, "check"=check hit); its only origin. Cache_Exe: [CACHE_CAP]str Cache_Id: [CACHE_CAP]str Cache_Ver: [CACHE_CAP]str Cache_Arch: [CACHE_CAP]str Cache_Src: [CACHE_CAP]str Cache_Disp: [CACHE_CAP]str Cache_Count: int = 0 # source literal: passed in by the scan side (VScan) / check side (VCheck); cache does not interpret it. # Cache files without the source field load as SOURCE_FALLBACK, keeping existing output unchanged. define SOURCE_FALLBACK = "workspace" # ---- table read API (for VJson to build JSON, without exposing the arrays) ---- def CacheCount() -> int: return Cache_Count def CacheExe(Idx: int) -> str: return Cache_Exe[Idx] def CacheId(Idx: int) -> str: return Cache_Id[Idx] def CacheVer(Idx: int) -> str: return Cache_Ver[Idx] def CacheArch(Idx: int) -> str: return Cache_Arch[Idx] def CacheSrc(Idx: int) -> str: return Cache_Src[Idx] def CacheDisp(Idx: int) -> str: return Cache_Disp[Idx] # ============================================================ # In-memory cache # ============================================================ def CacheFind(ExeAnsi: str) -> int: for i in range(Cache_Count): if string.strcmp(Cache_Exe[i], ExeAnsi) == 0: return i return -1 def FreeEntry(Idx: int): _mbuddy.free(rawptr(Cache_Exe[Idx])) _mbuddy.free(rawptr(Cache_Id[Idx])) _mbuddy.free(rawptr(Cache_Ver[Idx])) _mbuddy.free(rawptr(Cache_Arch[Idx])) _mbuddy.free(rawptr(Cache_Src[Idx])) _mbuddy.free(rawptr(Cache_Disp[Idx])) # Remove entry Idx (frees field copies and shifts later entries left). Returns 1=removed. # Single implementation shared by ValidateCache and VScan's per-round revalidation. def RemoveAt(Idx: int) -> int: global Cache_Count if Idx < 0 or Idx >= Cache_Count: return 0 FreeEntry(Idx) k: int = Idx while k < Cache_Count - 1: Cache_Exe[k] = Cache_Exe[k + 1] Cache_Id[k] = Cache_Id[k + 1] Cache_Ver[k] = Cache_Ver[k + 1] Cache_Arch[k] = Cache_Arch[k + 1] Cache_Src[k] = Cache_Src[k + 1] Cache_Disp[k] = Cache_Disp[k + 1] k += 1 Cache_Count -= 1 return 1 # Clear the in-memory table (frees all field copies). Used before re-reading cache.json each round. def ClearCache(): global Cache_Count for j in range(Cache_Count): FreeEntry(j) Cache_Count = 0 # Copy one discovery record into the cache (each field duplicated). 1=ok, 0=full/alloc failure. # Src: entry source ("workspace"/"check"), stored as-is and passed through on JSON output. def CacheAdd(ExeAnsi: str, Id: str, Ver: str, Arch: str, Src: str, Disp: str) -> int: global Cache_Count if Cache_Count >= CACHE_CAP: VLog.LogFmt1(" Warning: cache entries reached limit %d, new entry ignored\n", sizet(CACHE_CAP)) return 0 E: str = VFs.DupStr(ExeAnsi) if E is None: return 0 I: str = VFs.DupStr(Id) if I is None: _mbuddy.free(rawptr(E)) return 0 V: str = VFs.DupStr(Ver) if V is None: _mbuddy.free(rawptr(E)) _mbuddy.free(rawptr(I)) return 0 A: str = VFs.DupStr(Arch) if A is None: _mbuddy.free(rawptr(E)) _mbuddy.free(rawptr(I)) _mbuddy.free(rawptr(V)) return 0 S: str = VFs.DupStr(Src) if S is None: _mbuddy.free(rawptr(E)) _mbuddy.free(rawptr(I)) _mbuddy.free(rawptr(V)) _mbuddy.free(rawptr(A)) return 0 D: str = VFs.DupStr(Disp) if D is None: _mbuddy.free(rawptr(E)) _mbuddy.free(rawptr(I)) _mbuddy.free(rawptr(V)) _mbuddy.free(rawptr(A)) _mbuddy.free(rawptr(S)) return 0 Cache_Exe[Cache_Count] = E Cache_Id[Cache_Count] = I Cache_Ver[Cache_Count] = V Cache_Arch[Cache_Count] = A Cache_Src[Cache_Count] = S Cache_Disp[Cache_Count] = D Cache_Count += 1 return 1 # Drop cache entries whose exe no longer exists; returns the number removed def ValidateCache() -> int: removed: int = 0 j: int = 0 while j < Cache_Count: if VFs.PathIsFile(Cache_Exe[j]) != 0: j += 1 else: VLog.VerboseFmtS1(" [remove] no longer exists: %s\n", Cache_Exe[j]) RemoveAt(j) removed += 1 return removed # ============================================================ # Cache file # ============================================================ # Write EmitJson text (Buf, Len bytes) to the cache file (failure only warns, does not abort) def WriteCacheFile(Path: str, Buf: bytes, Len: sizet) -> int: if Buf is None: return 0 rc: int = VFs.WriteFileAll(Path, Buf, Len) if rc == 0: VLog.LogFmt1(" Warning: cache write failed (open failed, win32 err=%u)\n", wb.GetLastError()) return 0 if rc < 0: VLog.LogFmt2(" Warning: cache write incomplete (%zu/%zu bytes)\n", VFs.LastWriteDone, Len) return 0 return 1 # Return the string value for Key in Obj; missing / non-string / null => empty string. # Read-only field walk. def FieldStr(Obj: json.JsonValue*, Key: str) -> str: cu: json.JsonValue* = Obj.child while cu != None: if cu.key != None: if string.strcmp(cu.key, Key) == 0: if cu.vtype == json.JSON_STRING and cu.str_val != None: return cu.str_val return "" cu = cu.next return "" # Move a parsed JSON object into the cache table (fields read out, CacheAdd makes persistent copies). # Requires non-empty executable and id; missing/empty source => SOURCE_FALLBACK. def AddParsedEntry(Obj: json.JsonValue*, ConvBuf: bytes) -> int: Exe: str = FieldStr(Obj, "executable") Id: str = FieldStr(Obj, "id") if string.strlen(Exe) == 0 or string.strlen(Id) == 0: return 0 Src: str = FieldStr(Obj, "source") if string.strlen(Src) == 0: Src = SOURCE_FALLBACK # Match the scan side: store executable as ANSI (CacheFind compares with FindFirstFileA ANSI results) if VFs.Utf8ToAnsi(Exe, ConvBuf, VFs.CONV_CAP) == 0: VFs.CopyRaw(ConvBuf, VFs.CONV_CAP, Exe) return CacheAdd(str(ConvBuf), Id, FieldStr(Obj, "version"), FieldStr(Obj, "arch"), Src, FieldStr(Obj, "displayName")) # Parse cache text (top-level array of objects with string fields) via json.parse; returns entries added. # Memory: the tree lives on a per-call MemPool arena (freed whole); CacheAdd copies onto Vet_Mb. def ParseCacheText(Txt: str) -> int: if Txt is None: return 0 L: sizet = string.strlen(Txt) if L == 0: return 0 if L > sizet(CACHE_TEXT_MAX): VLog.LogFmt2("cache: text %zu bytes exceeds parse limit %zu, starting with empty cache\n", L, sizet(CACHE_TEXT_MAX)) return 0 # Pool capacity: worst case ~1 JsonValue node per input byte + key/value copies + temps, scaled # by CACHE_ARENA_PER_BYTE(128) to cover 8-byte alignment. A depleted MemPool would exit(1). Cap: sizet = L * sizet(CACHE_ARENA_PER_BYTE) + sizet(CACHE_ARENA_SLACK) Arena: bytes = stdlib.malloc(Cap) if Arena is None: VLog.LogStr("cache: parse pool allocation failed, starting with empty cache\n") return 0 Pool: memhub.MemPool* = memhub.MemPool(Arena, Cap) if Pool is None: stdlib.free(rawptr(Arena)) VLog.LogStr("cache: parse pool construction failed, starting with empty cache\n") return 0 # Pool precheck (fail-closed): the whole tree must fit, else json.parse's pool.alloc would # exit(1). Need mirrors Cap, so this fails cleanly (empty-cache fallback) if Cap is too small. Need: sizet = L * sizet(CACHE_ARENA_PER_BYTE) + sizet(CACHE_ARENA_SLACK) if Pool.AllocOk(Need) == 0: stdlib.free(rawptr(Arena)) VLog.LogFmt1("cache: parse pool precheck insufficient (worst case needs %zu bytes), starting with empty cache\n", Need) return 0 ConvBuf: bytes = _mbuddy.alloc(VFs.CONV_CAP) if ConvBuf is None: stdlib.free(rawptr(Arena)) VLog.LogStr("cache: encoding conversion buffer allocation failed, starting with empty cache\n") return 0 Root: json.JsonValue* = json.parse(Pool, Txt) added: int = 0 if Root == None: VLog.LogStr("cache: JSON parse failed (corrupt content), starting with empty cache\n") elif Root.vtype != json.JSON_ARRAY: VLog.LogStr("cache: top level is not a JSON array, starting with empty cache\n") else: cu: json.JsonValue* = Root.child while cu != None: if cu.vtype == json.JSON_OBJECT: added += AddParsedEntry(cu, ConvBuf) cu = cu.next _mbuddy.free(rawptr(ConvBuf)) stdlib.free(rawptr(Arena)) return added # Read the cache as a seed at startup. Missing/unreadable/unparsable are non-fatal: warn, start empty. def LoadCache(Path: str) -> int: buf: bytes = VFs.ReadWholeFile(Path, sizet(CACHE_FILE_MAX), wf.FILE_SHARE_READ | wf.FILE_SHARE_WRITE) if buf is None: rc_err: int = VFs.LastReadErr if rc_err == 1: VLog.VerboseFmtS1("cache: not found / cannot open, starting with empty cache: %s\n", Path) elif rc_err == 2: VLog.LogStr("cache: read size failed, starting with empty cache\n") elif rc_err == 5: VLog.LogStr("cache: buffer allocation failed, starting with empty cache\n") else: VLog.LogStr("cache: size invalid / too large, starting with empty cache\n") return 0 n: int = ParseCacheText(str(buf)) stdlib.free(rawptr(buf)) VLog.VerboseFmt1("cache: loaded %u seed entries\n", sizet(n)) return n # Re-read cache.json at round start: clear the in-memory table, then rebuild from the file. # Returns entries loaded; missing/unreadable/unparsable continue empty (LoadCache warns internally). def ReloadCache(Path: str) -> int: ClearCache() return LoadCache(Path)