import t, c from stdint import * import memhub import string import stdio import stdlib import w32.win32file as win32file import w32.win32base as win32base import hashlib import viperlib import lib.Projectrans.Utils as Utils import lib.core.VLogger as VLogger # ============================================================ # IncludesScanner - includes 目录递归扫描 # # 递归扫描 includes 目录,收集所有 .vp/.py 文件路径和 SHA1, # 为 Phase1 stub 生成和 Phase2 stub 合并提供基础数据。 # # 使用 Win32 FindFirstFileA/FindNextFileA 实现目录遍历。 # ============================================================ # 全局 mbuddy 指针 _mbuddy: memhub.MemBuddy | t.CPtr # 文件路径最大长度 MAX_PATH_LEN: t.CDefine = 512 # 单次扫描最大文件数 MAX_FILES: t.CDefine = 1024 # ============================================================ # FileEntry - 文件条目(路径 + SHA1) # ============================================================ @t.NoVTable class FileEntry: """扫描到的文件条目""" Path: str # 文件绝对路径 Sha1: str # SHA1 前16字符(16字节+null) RelPath: str # 相对于 includes 根目录的路径 ModuleName: str # 模块名(如 "ast.parser") IncDir: str # 所属 includes 根目录(多 includes 目录支持) # ============================================================ # ScanResult - 扫描结果 # ============================================================ @t.NoVTable class ScanResult: """扫描结果集合""" Entries: FileEntry | t.CPtr # FileEntry 数组 Count: t.CInt Capacity: t.CInt # ============================================================ # create_scan_result - 创建扫描结果容器 # ============================================================ def create_scan_result(pool: memhub.MemBuddy | t.CPtr) -> ScanResult | t.CPtr: """创建扫描结果容器,预分配 MAX_FILES 个槽位""" if pool is None: return None size: t.CSizeT = MAX_FILES * FileEntry.__sizeof__() # 使用 stdlib.malloc 避免 mbuddy 池耗尽(与 StubMerger 保持一致) entries: FileEntry | t.CPtr = stdlib.malloc(size) if entries is None: return None string.memset(entries, 0, size) result: ScanResult | t.CPtr = 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 - 向扫描结果添加文件条目 # ============================================================ def add_file_entry(result: ScanResult | t.CPtr, pool: memhub.MemBuddy | t.CPtr, abs_path: str, rel_path: str, sha1: str, inc_dir: str) -> int: """添加文件条目到扫描结果,返回 0 成功(inc_dir 为所属 includes 根目录)""" if result is None or pool is None: return 1 if result.Count >= result.Capacity: return 1 # 计算模块名:rel_path 中 / 替换为 .,去掉 .vp/.py 扩展名(__init__.vp 特判由 Utils 统一处理) mod_buf: str = Utils.PathToModuleName(rel_path, 0) if mod_buf is None: return 1 # 获取条目地址 entry_size: t.CSizeT = FileEntry.__sizeof__() entry_addr: t.CUInt64T = t.CUInt64T(result.Entries) + result.Count * entry_size entry: FileEntry | t.CPtr = (FileEntry | t.CPtr)(t.CVoid(entry_addr, t.CPtr)) if entry is None: return 1 # 复制路径字符串 abs_len: t.CSizeT = 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 # 复制 SHA1 sha1_buf: str = stdlib.malloc(17) if sha1_buf is None: return 1 string.strcpy(sha1_buf, sha1) entry.Sha1 = sha1_buf # 复制相对路径 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 # 复制模块名 entry.ModuleName = mod_buf # 复制所属 includes 根目录(多 includes 目录支持,Phase2 拼路径用) if inc_dir is not None: inc_len: t.CSizeT = 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 - 读取文件内容并计算 SHA1 # # 与 Projectrans.py 一致:CRLF → LF 转换后计算 SHA1。 # Projectrans.py 用 Python 文本模式读取(自动 CRLF→LF), # IncludesScanner 用二进制模式读取,需手动去除 \r。 # ============================================================ def compute_file_sha1(pool: memhub.MemBuddy | t.CPtr, file_path: str) -> str: """读取文件内容并计算 SHA1 前16字符(CRLF→LF 后计算,与 Projectrans.py 一致)""" if pool is None or file_path is None: return None # 读取文件内容(stdlib.malloc 分配,避免 mbuddy 池耗尽) # BUF_SIZE 与翻译侧 SRC_BUF_SIZE(1MB)一致:若截断到 128KB, # 超过 128KB 的文件尾部内容变更不会改变 SHA1 → 增量缓存永不失效。 BUF_SIZE: t.CSizeT = 1048576 buf: bytes = Utils.read_file_to_buf(file_path, BUF_SIZE) if buf is None: return None # 原地去除 \r(CRLF → LF),与 Projectrans.py 文本模式读取一致 buf_len: t.CSizeT = string.strlen(buf) write_pos: t.CSizeT = 0 for ch in buf: if ch != '\r': buf[write_pos] = ch write_pos += 1 # 添加 null 终止符 buf[write_pos] = 0 # 计算 SHA1 ctx: hashlib.sha1 | t.CPtr = 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) # 转为十六进制字符串(取前 8 字节 = 16 个十六进制字符,stdlib 分配保持原行为) hex_buf: str = Utils.bytes_to_hex(pool, digest, 8, 0, 1) stdlib.free(buf) stdlib.free(digest) return hex_buf # ============================================================ # scan_directory_recursive - 递归扫描目录 # # 使用 FindFirstFileA/FindNextFileA 遍历目录树, # 对每个 .vp/.py 文件计算 SHA1 并添加到结果中。 # ============================================================ def scan_directory_recursive(pool: memhub.MemBuddy | t.CPtr, root_dir: str, rel_prefix: str, result: ScanResult | t.CPtr, inc_dir: str) -> int: """递归扫描目录,收集 .vp/.py 文件(inc_dir 为所属 includes 根目录)""" if pool is None or root_dir is None or result is None: return 1 # 子目录递归失败计数(不静默忽略,汇总处 fail-fast 终止) subdir_failed: int = 0 # 构造搜索模式: root_dir/* root_len: t.CSizeT = 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) # 使用 FindFirstFileA 开始搜索 find_data: win32file.WIN32_FIND_DATAA | t.CPtr = stdlib.malloc(win32file.WIN32_FIND_DATAA.__sizeof__()) if find_data is None: return 1 string.memset(find_data, 0, win32file.WIN32_FIND_DATAA.__sizeof__()) handle: win32base.HANDLE = win32file.FindFirstFileA(pattern, find_data) if handle == win32base.INVALID_HANDLE_VALUE: return 1 # 遍历所有文件和子目录 while True: # 跳过 . 和 .. fname: str = find_data.cFileName if fname is not None: fname0: t.CChar = fname[0] if fname0 == '.': fname1: t.CChar = fname[1] if fname1 == '\0': # "." if win32file.FindNextFileA(handle, find_data) == 0: break continue elif fname1 == '.': fname2: t.CChar = fname[2] if fname2 == '\0': # ".." if win32file.FindNextFileA(handle, find_data) == 0: break continue # 检查是否为目录 is_dir: int = find_data.dwFileAttributes & win32file.FILE_ATTRIBUTE_DIRECTORY # 构造完整路径 fname_len: t.CSizeT = string.strlen(fname) full_path: bytes = Utils.JoinPath(root_dir, fname) if full_path is None: break # 构造相对路径 prefix_len: t.CSizeT = 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: # 递归扫描子目录 if scan_directory_recursive(pool, full_path, rel_path, result, inc_dir) != 0: subdir_failed += 1 else: # 检查是否为 .vp 源文件(Viper 源仅 .vp,不再识别 .py) is_py: int = 0 if Utils.is_vp_at(fname, 0, fname_len) != 0: is_py = 1 if is_py != 0: # 跳过 __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: # 计算 SHA1 sha1: str = compute_file_sha1(pool, full_path) if sha1 is not None: add_file_entry(result, pool, full_path, rel_path, sha1, inc_dir) VLogger.debug(f"扫描: {rel_path} -> {sha1}", "scan") else: VLogger.warning(f"SHA1 计算失败: {full_path}", "scan") # 继续搜索下一个文件 if win32file.FindNextFileA(handle, find_data) == 0: break win32file.FindClose(handle) if subdir_failed > 0: VLogger.error(f"子目录扫描失败 {subdir_failed} 个", "scan") return 0 # ============================================================ # scan_includes - 扫描 includes 目录入口 # # 扫描指定的 includes 目录,返回所有 .vp/.py 文件的路径和 SHA1。 # ============================================================ def scan_includes(pool: memhub.MemBuddy | t.CPtr, includes_dirs: str) -> ScanResult | t.CPtr: """扫描 includes 目录(支持空格分隔的多目录),返回所有 .vp/.py 文件的扫描结果""" if pool is None or includes_dirs is None: return None result: ScanResult | t.CPtr = create_scan_result(pool) if result is None: return None # 解析空格分隔的多 includes 目录(如 "../../includes ./includes"),逐目录递归扫描合并 dirs_total: t.CSizeT = string.strlen(includes_dirs) d_i: t.CSizeT = 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: t.CSizeT = d_i while d_i < dirs_total and includes_dirs[d_i] != ' ': d_i += 1 d_len: t.CSizeT = d_i - d_start if d_len == 0: continue dir_buf: bytes = stdlib.malloc(d_len + 1) if dir_buf is None: VLogger.error("includes 目录分配失败,终止扫描", "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 - 按模块名查找文件条目 # ============================================================ def find_entry_by_module(result: ScanResult | t.CPtr, module_name: str) -> FileEntry | t.CPtr: """按模块名查找文件条目""" if result is None or module_name is None: return None entry_size: t.CSizeT = FileEntry.__sizeof__() for i in range(result.Count): entry_addr: t.CUInt64T = t.CUInt64T(result.Entries) + i * entry_size entry: FileEntry | t.CPtr = (FileEntry | t.CPtr)(t.CVoid(entry_addr, t.CPtr)) if entry is not None: if entry.ModuleName is not None: if string.strcmp(entry.ModuleName, module_name) == 0: return entry return None