Files
ViperC/App/lib/core/IncludesScanner.vp

405 lines
16 KiB
Plaintext

from stdint import *
import memhub
import string
import stdlib
import win32.win32file as win32file
import win32.win32base as win32base
import win32.fileio as fileio
import hashlib
import viperlib
import lib.Projectrans.Utils as Utils
import lib.core.VLogger as VLogger
# IncludesScanner - recursive scan of the includes directory (Win32 FindFirstFileA/
# FindNextFileA); collects .vp paths and SHA1s for Phase1 stub gen / Phase2 merge.
# global mbuddy pointer
_mbuddy: memhub.MemBuddy*
# max file path length
define MAX_PATH_LEN = 512
# max files per scan
define MAX_FILES = 1024
# FileEntry - file entry (path + SHA1)
novirtual class FileEntry:
"""A scanned file entry."""
Path: str # absolute file path
Sha1: str # first 16 SHA1 chars (16 bytes + null)
RelPath: str # path relative to the includes root
ModuleName: str # module name (e.g. "ast.parser")
IncDir: str # owning includes root dir (multi-root support)
# ScanResult - scan result
novirtual class ScanResult:
"""A collection of scan results."""
Entries: FileEntry* # FileEntry array
Count: int
Capacity: int
# create_scan_result - create the scan result container
def create_scan_result(pool: memhub.MemBuddy*) -> ScanResult*:
"""Create the scan result container with MAX_FILES preallocated slots."""
if pool is None: return None
size: sizet = MAX_FILES * FileEntry.__sizeof__()
# use stdlib.malloc to avoid exhausting the mbuddy pool (consistent with StubMerger)
entries: FileEntry* = stdlib.malloc(size)
if entries is None: return None
string.memset(entries, 0, size)
result: ScanResult* = stdlib.malloc(ScanResult.__sizeof__())
if result is None: return None
string.memset(result, 0, ScanResult.__sizeof__())
result.Entries = entries
result.Count = 0
result.Capacity = MAX_FILES
return result
# add_file_entry - append a file entry to the scan result
def add_file_entry(result: ScanResult*, pool: memhub.MemBuddy*, abs_path: str, rel_path: str, sha1: str, inc_dir: str) -> int:
"""Append a file entry to the scan result; inc_dir is the owning includes root."""
if result is None or pool is None: return 1
if result.Count >= result.Capacity: return 1
# compute module name: '/' -> '.', strip .vp/.py (Utils handles __init__.vp)
mod_buf: str = Utils.PathToModuleName(rel_path, 0)
if mod_buf is None: return 1
# get the entry address
entry_size: sizet = FileEntry.__sizeof__()
entry_addr: uint64 = uint64(result.Entries) + result.Count * entry_size
entry: FileEntry* = (FileEntry*)(entry_addr)
if entry is None: return 1
# copy the path string
abs_len: sizet = string.strlen(abs_path)
abs_buf: str = stdlib.malloc(abs_len + 1)
if abs_buf is None: return 1
string.strcpy(abs_buf, abs_path)
entry.Path = abs_buf
# copy the SHA1
sha1_buf: str = stdlib.malloc(17)
if sha1_buf is None: return 1
string.strcpy(sha1_buf, sha1)
entry.Sha1 = sha1_buf
# copy the relative path
rel_buf: str = stdlib.malloc(string.strlen(rel_path) + 1)
if rel_buf is None: return 1
string.strcpy(rel_buf, rel_path)
entry.RelPath = rel_buf
# copy the module name
entry.ModuleName = mod_buf
# copy the owning includes root dir (used by Phase2 path joining)
if inc_dir is not None:
inc_len: sizet = string.strlen(inc_dir)
inc_buf: str = stdlib.malloc(inc_len + 1)
if inc_buf is None: return 1
string.strcpy(inc_buf, inc_dir)
entry.IncDir = inc_buf
result.Count += 1
return 0
# compute_file_sha1 - read file content and compute SHA1 (CRLF->LF, matching Projectrans.py)
def compute_file_sha1(pool: memhub.MemBuddy*, file_path: str) -> str:
"""Read a file, normalize CRLF to LF, and return the first 16 SHA1 hex chars."""
if pool is None or file_path is None: return None
# read file content; BUF_SIZE must match SRC_BUF_SIZE (1MB) or trailing edits won't change SHA1
BUF_SIZE: sizet = 1048576
# Refuse larger files: hashing only the first 1 MiB gives two files that differ later the same
# SHA1, and the SHA1 keys the stub/cache files (colliding modules would reuse each other's IR).
sf_sz: fileio.File* = fileio.File(file_path, fileio.MODE.R)
if sf_sz.closed: return None
too_big: int = 0
if sf_sz.size() >= BUF_SIZE: too_big = 1
sf_sz.close()
if too_big != 0:
VLogger.error(f"source file exceeds the {BUF_SIZE}-byte limit and cannot be hashed completely: {file_path}", "IncludesScanner")
return None
buf: bytes = Utils.read_file_to_buf(file_path, BUF_SIZE)
if buf is None: return None
# strip \r in place (CRLF -> LF), matching Projectrans.py text-mode reads
buf_len: sizet = string.strlen(buf)
write_pos: sizet = 0
for ch in buf:
if ch != '\r':
buf[write_pos] = ch
write_pos += 1
# add a null terminator
buf[write_pos] = 0
# compute SHA1
ctx: hashlib.sha1* = hashlib.sha1()
if ctx is None:
stdlib.free(buf)
return None
ctx.update(buf)
digest: bytes = stdlib.malloc(hashlib.SHA1_DIGEST_LEN)
if digest is None:
stdlib.free(buf)
return None
ctx.final(digest)
# convert to hex (first 8 bytes = 16 hex chars; stdlib allocation unchanged)
hex_buf: str = Utils.bytes_to_hex(pool, digest, 8, 0, 1)
stdlib.free(buf)
stdlib.free(digest)
return hex_buf
# scan_directory_recursive - recursively scan a directory (FindFirstFileA/FindNextFileA)
def scan_directory_recursive(pool: memhub.MemBuddy*, root_dir: str, rel_prefix: str, result: ScanResult*, inc_dir: str) -> int:
"""Recursively collect .vp/.py files under a directory; inc_dir is the includes root."""
if pool is None or root_dir is None or result is None: return 1
# subdir recursion failure count (not ignored; fail-fast at the summary)
subdir_failed: int = 0
# build search pattern: root_dir/*
root_len: sizet = string.strlen(root_dir)
pattern: bytes = stdlib.malloc(root_len + 4)
if pattern is None: return 1
viperlib.snprintf(pattern, root_len + 4, "%s/*", root_dir)
# find_data buffer +16 slack: VPC's WIN32_FIND_DATAA.__sizeof__() undercounts Windows writes
fd_size_s: sizet = win32file.WIN32_FIND_DATAA.__sizeof__()
find_data: win32file.WIN32_FIND_DATAA* = stdlib.malloc(fd_size_s + 16)
if find_data is None: return 1
string.memset(find_data, 0, fd_size_s + 16)
handle: win32base.HANDLE = win32file.FindFirstFileA(pattern, find_data)
if handle == win32base.INVALID_HANDLE_VALUE: return 1
# iterate all files and subdirectories
while True:
# skip . and ..
fname: str = find_data.cFileName
if fname is not None:
fname0: char = fname[0]
if fname0 == '.':
fname1: char = fname[1]
if fname1 == '\0':
# "."
if win32file.FindNextFileA(handle, find_data) == 0: break
continue
elif fname1 == '.':
fname2: char = fname[2]
if fname2 == '\0':
# ".."
if win32file.FindNextFileA(handle, find_data) == 0: break
continue
# check whether it is a directory
is_dir: int = find_data.dwFileAttributes & win32file.FILE_ATTRIBUTE_DIRECTORY
# build the full path
fname_len: sizet = string.strlen(fname)
full_path: bytes = Utils.JoinPath(root_dir, fname)
if full_path is None: break
# build the relative path
prefix_len: sizet = 0
if rel_prefix is not None: prefix_len = string.strlen(rel_prefix)
rel_path: bytes = None
if rel_prefix is not None and prefix_len > 0: rel_path = Utils.JoinPath(rel_prefix, fname)
else:
rel_path = stdlib.malloc(fname_len + 1)
if rel_path is None: break
string.strcpy(rel_path, fname)
if rel_path is None: break
if is_dir != 0:
# recurse into the subdirectory
if scan_directory_recursive(pool, full_path, rel_path, result, inc_dir) != 0: subdir_failed += 1
else:
# check for a .vp source file (Viper uses .vp only; no longer .py)
is_py: int = 0
if Utils.is_vp_at(fname, 0, fname_len) != 0: is_py = 1
if is_py != 0:
# skip files under __pycache__
is_pycache: int = 0
if rel_prefix is not None:
if string.strcmp(rel_prefix, "__pycache__") == 0: is_pycache = 1
if is_pycache == 0:
# compute SHA1
sha1: str = compute_file_sha1(pool, full_path)
if sha1 is not None:
# fail-fast: a full table used to drop the file silently, so it never got a
# SHA1 entry / dependency edge and was therefore never compiled.
if add_file_entry(result, pool, full_path, rel_path, sha1, inc_dir) != 0:
VLogger.error(f"includes scan could not register '{full_path}' (the {MAX_FILES}-file capacity or an allocation failed); raise MAX_FILES", "scan")
VLogger.debug(f"scan: {rel_path} -> {sha1}", "scan")
else: VLogger.warning(f"SHA1 computation failed: {full_path}", "scan")
# continue to the next file
if win32file.FindNextFileA(handle, find_data) == 0: break
win32file.FindClose(handle)
if subdir_failed > 0: VLogger.error(f"subdirectory scan failed for {subdir_failed} entries", "scan")
return 0
# scan_includes - scan entry point: return all .vp file paths and SHA1s
def scan_includes(pool: memhub.MemBuddy*, includes_dirs: str) -> ScanResult*:
"""Scan space-separated includes dirs and return all scanned .vp/.py files."""
if pool is None or includes_dirs is None: return None
result: ScanResult* = create_scan_result(pool)
if result is None: return None
# parse space-separated includes dirs, recursively scanning and merging each
dirs_total: sizet = string.strlen(includes_dirs)
d_i: sizet = 0
while d_i < dirs_total:
while d_i < dirs_total and includes_dirs[d_i] == ' ': d_i += 1
if d_i >= dirs_total: break
d_start: sizet = d_i
while d_i < dirs_total and includes_dirs[d_i] != ' ': d_i += 1
d_len: sizet = d_i - d_start
if d_len == 0: continue
dir_buf: bytes = stdlib.malloc(d_len + 1)
if dir_buf is None: VLogger.error("failed to allocate the includes directory; aborting scan", "scan")
string.strncpy(dir_buf, includes_dirs + d_start, d_len)
dir_buf[d_len] = '\0'
scan_directory_recursive(pool, dir_buf, None, result, dir_buf)
stdlib.free(dir_buf)
return result
# find_entry_by_module - find a file entry by module name
def find_entry_by_module(result: ScanResult*, module_name: str) -> FileEntry*:
"""Find a file entry by module name."""
if result is None or module_name is None: return None
entry_size: sizet = FileEntry.__sizeof__()
for i in range(int(result.Count)):
entry_addr: uint64 = uint64(result.Entries) + i * entry_size
entry: FileEntry* = (FileEntry*)(entry_addr)
if entry is not None:
if entry.ModuleName is not None:
if string.strcmp(entry.ModuleName, module_name) == 0: return entry
return None
# _PathExistsA - path existence check (GetFileAttributesA)
def _PathExistsA(path: str) -> int:
"""Return 1 if the path exists, else 0, using GetFileAttributesA."""
if path is None: return 0
attr: ULONG = win32file.GetFileAttributesA(path)
if attr == 0xFFFFFFFF: # INVALID_FILE_ATTRIBUTES
return 0
return 1
# _TryResolveModuleFile - try one candidate path and write outputs (1=hit, 0=miss);
# out_abs_path/out_rel_path/out_inc_dir are rawptr output slots.
def _TryResolveModuleFile(inc_dir: str, rel_seg: bytes, rel_len: sizet, suffix: str, out_abs_path: rawptr, out_rel_path: rawptr, out_inc_dir: rawptr) -> int:
if inc_dir is None or rel_seg is None or suffix is None: return 0
inc_len: sizet = string.strlen(inc_dir)
suf_len: sizet = string.strlen(suffix)
# candidate absolute path: {inc}/{rel}{suffix} (hand-built to avoid %. *s)
cand_len: sizet = inc_len + 1 + rel_len + suf_len
cand: bytes = stdlib.malloc(cand_len + 1)
if cand is None: return 0
string.strcpy(cand, inc_dir)
cand[inc_len] = '/'
string.strncpy(cand + inc_len + 1, rel_seg, rel_len)
string.strcpy(cand + inc_len + 1 + rel_len, suffix)
if _PathExistsA(cand) == 0:
stdlib.free(cand)
return 0
# path relative to the includes root: {rel}{suffix}
rel_path: bytes = stdlib.malloc(rel_len + suf_len + 1)
if rel_path is None:
stdlib.free(cand)
return 0
string.strncpy(rel_path, rel_seg, rel_len)
rel_path[rel_len] = '\0'
string.strcat(rel_path, suffix)
# Deep-copy the owning includes root dir; allocate everything before writing
# slots to avoid a half-written state when inc allocation fails.
inc_buf: bytes = None
if out_inc_dir is not None:
inc_buf = stdlib.malloc(inc_len + 1)
if inc_buf is None:
stdlib.free(cand)
stdlib.free(rel_path)
return 0
string.strcpy(inc_buf, inc_dir)
# Write output slots (stdlib deep copies; caller frees). Slots must be cast to
# (sizet*): rawptr* folds to rawptr and [0] becomes a 1-byte access.
if out_abs_path is not None:
abs_slot: sizet* = (sizet*)(out_abs_path)
abs_slot[0] = sizet(cand)
else: stdlib.free(cand)
if out_rel_path is not None:
rel_slot: sizet* = (sizet*)(out_rel_path)
rel_slot[0] = sizet(rel_path)
else: stdlib.free(rel_path)
if out_inc_dir is not None and inc_buf is not None:
inc_slot: sizet* = (sizet*)(out_inc_dir)
inc_slot[0] = sizet(inc_buf)
return 1
# ResolveModuleToPath - module name -> file path, resolved on demand (no full scan).
# Tries each includes dir, preferring file modules over packages. Returns 0 found, 1 not.
def ResolveModuleToPath(includes_dirs: str, mod_name: str, out_abs_path: rawptr, out_rel_path: rawptr, out_inc_dir: rawptr) -> int:
if includes_dirs is None or mod_name is None: return 1
mn_len: sizet = string.strlen(mod_name)
if mn_len == 0: return 1
# mod_name -> relative path segment ('.' -> '/'): ast.parser -> ast/parser
rel_seg: bytes = stdlib.malloc(mn_len + 1)
if rel_seg is None: return 1
rel_len: sizet = 0
for mi in range(int(mn_len)):
ch_m: char = mod_name[mi]
if ch_m == '.': rel_seg[rel_len] = '/'
else: rel_seg[rel_len] = ch_m
rel_len += 1
rel_seg[rel_len] = '\0'
found: int = 0
d_total: sizet = string.strlen(includes_dirs)
d_i: sizet = 0
while d_i < d_total:
while d_i < d_total and includes_dirs[d_i] == ' ': d_i += 1
if d_i >= d_total: break
d_start: sizet = d_i
while d_i < d_total and includes_dirs[d_i] != ' ': d_i += 1
d_len: sizet = d_i - d_start
if d_len == 0: continue
inc_dir_buf: bytes = stdlib.malloc(d_len + 1)
if inc_dir_buf is None: continue
string.strncpy(inc_dir_buf, includes_dirs + d_start, d_len)
inc_dir_buf[d_len] = '\0'
# candidate 1: {inc}/{rel}.vp (file module)
if _TryResolveModuleFile(inc_dir_buf, rel_seg, rel_len, ".vp", out_abs_path, out_rel_path, out_inc_dir) != 0:
found = 1
stdlib.free(inc_dir_buf)
break
# candidate 2: {inc}/{rel}/__init__.vp (package module)
if _TryResolveModuleFile(inc_dir_buf, rel_seg, rel_len, "/__init__.vp", out_abs_path, out_rel_path, out_inc_dir) != 0:
found = 1
stdlib.free(inc_dir_buf)
if found != 0: break
stdlib.free(rel_seg)
if found != 0: return 0
return 1