diff --git a/win32/fileio.vp b/win32/fileio.vp new file mode 100644 index 0000000..d6ad77e --- /dev/null +++ b/win32/fileio.vp @@ -0,0 +1,449 @@ +from stdint import * +from win32.win32base import HANDLE, LPCSTR, LPCWSTR +import win32.win32base +import stdlib +import string +import win32.win32file + + +enum MODE: + R, W, A + RP, WP, AP + X, XP + + +enum FRESULT: + OK = 0 + ERR = -1 + ERR_CLOSED = -2 + ERR_PERM = -3 + ERR_IO = -4 + ERR_NOTFOUND = -5 + + +define SEEK_SET = 0 +define SEEK_CUR = 1 +define SEEK_END = 2 + +define INVALID_SET_FILE_POINTER = -1 + +define SHARE_READ = 0x00000001 +define SHARE_WRITE = 0x00000002 +define SHARE_DELETE = 0x00000004 + +# ============================================================================= +# Environment variable expansion (the only implementation in the path layer). +# +# Expands "%VAR%" in paths before opening a file (Win32 only called when the path contains '%'); returns a stdlib.malloc'd string (caller frees) or None on failure / no expansion. +# UNC, relative, drive, ".." and long paths are passed through unchanged; A-series results share the ANSI code page of existing A-series call sites. +# ============================================================================= +extern def ExpandEnvironmentStringsA(lpSrc: LPCSTR, lpDst: CHARPTR, nSize: ULONG) -> ULONG + +# Expansion buffer cap (incl. NUL); exceeding it is treated as failure and falls back to the original path +define EXPAND_ENV_CAP = 4096 + + +def ExpandEnvironmentPath(path: str) -> str: + """Expand env vars when path contains '%': returns a new string (caller stdlib.free) or None if not needed / on failure""" + if path is None: return None + # Fast path: no '%' -> return None (no Win32 call, path unchanged) + if string.strchr(path, '%') is None: return None + buf: str = stdlib.malloc(EXPAND_ENV_CAP) + if buf is None: return None + n: ULONG = ExpandEnvironmentStringsA(path, buf, ULONG(EXPAND_ENV_CAP)) + # n == 0: failure; n > cap: required TCHAR count (incl. NUL) exceeds buffer, content unusable + if n == 0 or int(n) > EXPAND_ENV_CAP: + stdlib.free(buf) + return None + return buf + + +def create_file_utf8(path: str, access: ULONG, share: ULONG, disposition: ULONG, flags: ULONG) -> HANDLE: + """Open/create a file with path-encoding fallback: '%' paths are expanded first (original on failure); valid UTF-8 -> UTF-16 CreateFileW, invalid UTF-8 (ANSI argv) -> CreateFileA. See win32base.""" + # Expanded string must live past the CreateFile call (kernel holds the handle; the string is discarded after) + ep: str = ExpandEnvironmentPath(path) + use_path: str = path + if ep is not None: + use_path = ep + h: HANDLE = None + if win32.win32base.utf8_is_valid(use_path) == 0: + h = win32.win32file.CreateFileA(use_path, access, share, None, disposition, flags, None) + else: + wp: WCHARPTR = win32.win32base.utf8_to_utf16_dup(use_path) + if wp is None: + # Wide-char buffer alloc failed: fall back to A path; non-ASCII paths fail naturally and return + # INVALID_HANDLE_VALUE (error not masked, just the same failure exit). + h = win32.win32file.CreateFileA(use_path, access, share, None, disposition, flags, None) + else: + h = win32.win32file.CreateFileW(wp, access, share, None, disposition, flags, None) + stdlib.free(wp) + if ep is not None: + stdlib.free(ep) + return h + + +class File: + handle: HANDLE + closed: bool = True + can_read: bool = False + can_write: bool = False + is_append: bool = False + _share_mode: ULONG + + def __init__(self, filename: str, mode: ULONG, share: ULONG = SHARE_READ): + self._share_mode = share + + access: ULONG = 0 + disposition: ULONG = win32.win32file.OPEN_EXISTING + + if mode == MODE.R: + access = win32.win32file.GENERIC_READ + disposition = win32.win32file.OPEN_EXISTING + self.can_read = True + elif mode == MODE.W: + access = win32.win32file.GENERIC_WRITE + disposition = win32.win32file.CREATE_ALWAYS + self.can_write = True + elif mode == MODE.A: + access = win32.win32file.GENERIC_WRITE + disposition = win32.win32file.OPEN_ALWAYS + self.can_write = True + self.is_append = True + elif mode == MODE.RP: + access = win32.win32file.GENERIC_READ | win32.win32file.GENERIC_WRITE + disposition = win32.win32file.OPEN_EXISTING + self.can_read = True + self.can_write = True + elif mode == MODE.WP: + access = win32.win32file.GENERIC_READ | win32.win32file.GENERIC_WRITE + disposition = win32.win32file.CREATE_ALWAYS + self.can_read = True + self.can_write = True + elif mode == MODE.AP: + access = win32.win32file.GENERIC_READ | win32.win32file.GENERIC_WRITE + disposition = win32.win32file.OPEN_ALWAYS + self.can_read = True + self.can_write = True + self.is_append = True + elif mode == MODE.X: + access = win32.win32file.GENERIC_WRITE + disposition = win32.win32file.CREATE_NEW + self.can_write = True + elif mode == MODE.XP: + access = win32.win32file.GENERIC_READ | win32.win32file.GENERIC_WRITE + disposition = win32.win32file.CREATE_NEW + self.can_read = True + self.can_write = True + else: + access = win32.win32file.GENERIC_READ + disposition = win32.win32file.OPEN_EXISTING + self.can_read = True + + self.handle = create_file_utf8( + filename, access, self._share_mode, disposition, + win32.win32file.FILE_ATTRIBUTE_NORMAL + ) + + if self.handle == win32.win32base.INVALID_HANDLE_VALUE: + self.closed = True + return + + self.closed = False + + if self.is_append: + self.seek(0, SEEK_END) + + def __enter__(self) -> File*: + return self + + def __exit__(self): + self.close() + + def read(self, buf: bytes, count: ULONG) -> LONG: + if self.closed: return FRESULT.ERR_CLOSED + if not self.can_read: return FRESULT.ERR_PERM + bytes_read: ULONG = 0 + result: BOOL = win32.win32file.ReadFile(self.handle, buf, count, &bytes_read, None) + if result == 0: return FRESULT.ERR_IO + return LONG(bytes_read) + + def write(self, buf: bytes, count: ULONG) -> LONG: + if self.closed: return FRESULT.ERR_CLOSED + if not self.can_write: return FRESULT.ERR_PERM + bytes_written: ULONG = 0 + result: BOOL = win32.win32file.WriteFile(self.handle, buf, count, &bytes_written, None) + if result == 0: return FRESULT.ERR_IO + return LONG(bytes_written) + + def write_str(self, s: str) -> LONG: + if self.closed: return FRESULT.ERR_CLOSED + if not self.can_write: return FRESULT.ERR_PERM + length: ULONG = 0 + p: bytes = s + while *p != 0: + length += 1 + p += 1 + return self.write(s, length) + + def seek(self, offset: LONG, origin: ULONG) -> LONG: + if self.closed: return FRESULT.ERR_CLOSED + result: LONG = win32.win32file.SetFilePointer(self.handle, offset, None, origin) + if result == -1: return FRESULT.ERR_IO + return result + + def tell(self) -> LONG: + if self.closed: return FRESULT.ERR_CLOSED + return self.seek(0, SEEK_CUR) + + def close(self) -> LONG: + if self.closed: return FRESULT.ERR_CLOSED + win32.win32base.CloseHandle(self.handle) + self.closed = True + return FRESULT.OK + + def flush(self) -> LONG: + if self.closed: return FRESULT.ERR_CLOSED + win32.win32file.FlushFileBuffers(self.handle) + return FRESULT.OK + + def size(self) -> LONGLONG: + if self.closed: return -1 + li: win32.win32base.LARGE_INTEGER = win32.win32base.LARGE_INTEGER() + result: BOOL = win32.win32file.GetFileSizeEx(self.handle, &li) + if result == 0: return -1 + return li.QuadPart + + def readline(self, buf: bytes, max_count: ULONG) -> LONG: + if self.closed: return FRESULT.ERR_CLOSED + if not self.can_read: return FRESULT.ERR_PERM + if max_count < 2: return FRESULT.ERR + total: ULONG = 0 + bytes_read: ULONG = 0 + ch: int = 0 + while total < max_count - 1: + bytes_read = 0 + result: BOOL = win32.win32file.ReadFile(self.handle, buf + total, 1, &bytes_read, None) + if result == 0 or bytes_read == 0: break + # Explicit byte read: indexing loads 1 byte by the pointer element type (i8), same as Repl.vp _read_line. + # Do not use c.Deref(buf + total): its width depends on whether the pointer is i8*, but 1 byte is required here. + ch = buf[total] + total += 1 + if ch == 10: break + if total < max_count: + buf[total] = 0 + return LONG(total) + + def read_all(self, buf: bytes, buf_size: sizet) -> LONG: + """Read the whole file into buf as a C string (seeks to 0 first); returns bytes read, negative on error. + buf_size is capacity incl. terminator (sizet: same width as f.size(), so a 64-bit size is NOT + silently narrowed on the way in): reads at most buf_size-1 bytes then writes a NUL at buf[ret]. + A file longer than buf_size-1 is silently truncated; compare f.size() first to detect truncation. + A single ReadFile is capped at DWORD; a capacity above 4GiB-1 is treated as a failure here.""" + if self.closed: return FRESULT.ERR_CLOSED + if not self.can_read: return FRESULT.ERR_PERM + if buf_size < 2: return FRESULT.ERR + file_size: LONGLONG = self.size() + if file_size < 0: return FRESULT.ERR_IO + # 本次可读字节数 = min(file_size, buf_size-1):全程用 sizet 计算,只在交给 + # ReadFile(ULONG) 处显式窄化(单次读上限 DWORD,故超限直接失败,不静默截断)。 + to_read_sz: sizet = buf_size - 1 + if sizet(file_size) < to_read_sz: + to_read_sz = sizet(file_size) + if to_read_sz > 0xFFFFFFFF: return FRESULT.ERR + to_read: ULONG = ULONG(to_read_sz) + self.seek(0, SEEK_SET) + bytes_read: ULONG = 0 + result: BOOL = win32.win32file.ReadFile(self.handle, buf, to_read, &bytes_read, None) + if result == 0: return FRESULT.ERR_IO + if sizet(bytes_read) < buf_size: + buf[bytes_read] = 0 + return LONG(bytes_read) + + +class FileW: + handle: HANDLE + closed: bool + can_read: bool + can_write: bool + is_append: bool + _share_mode: ULONG + + def __init__(self, filename: LPCWSTR, mode: ULONG, share: ULONG = SHARE_READ): + self.closed = True + self.can_read = False + self.can_write = False + self.is_append = False + self._share_mode = share + + access: ULONG = 0 + disposition: ULONG = win32.win32file.OPEN_EXISTING + + if mode == MODE.R: + access = win32.win32file.GENERIC_READ + disposition = win32.win32file.OPEN_EXISTING + self.can_read = True + elif mode == MODE.W: + access = win32.win32file.GENERIC_WRITE + disposition = win32.win32file.CREATE_ALWAYS + self.can_write = True + elif mode == MODE.A: + access = win32.win32file.GENERIC_WRITE + disposition = win32.win32file.OPEN_ALWAYS + self.can_write = True + self.is_append = True + elif mode == MODE.RP: + access = win32.win32file.GENERIC_READ | win32.win32file.GENERIC_WRITE + disposition = win32.win32file.OPEN_EXISTING + self.can_read = True + self.can_write = True + elif mode == MODE.WP: + access = win32.win32file.GENERIC_READ | win32.win32file.GENERIC_WRITE + disposition = win32.win32file.CREATE_ALWAYS + self.can_read = True + self.can_write = True + elif mode == MODE.AP: + access = win32.win32file.GENERIC_READ | win32.win32file.GENERIC_WRITE + disposition = win32.win32file.OPEN_ALWAYS + self.can_read = True + self.can_write = True + self.is_append = True + elif mode == MODE.X: + access = win32.win32file.GENERIC_WRITE + disposition = win32.win32file.CREATE_NEW + self.can_write = True + elif mode == MODE.XP: + access = win32.win32file.GENERIC_READ | win32.win32file.GENERIC_WRITE + disposition = win32.win32file.CREATE_NEW + self.can_read = True + self.can_write = True + else: + access = win32.win32file.GENERIC_READ + disposition = win32.win32file.OPEN_EXISTING + self.can_read = True + + self.handle = win32.win32file.CreateFileW( + filename, access, self._share_mode, + None, disposition, win32.win32file.FILE_ATTRIBUTE_NORMAL, None + ) + + if self.handle == win32.win32base.INVALID_HANDLE_VALUE: + self.closed = True + return + + self.closed = False + + if self.is_append: + self.seek(0, SEEK_END) + + def __enter__(self) -> FileW*: + return self + + def __exit__(self): + self.close() + + def read(self, buf: bytes, count: ULONG) -> LONG: + if self.closed: return FRESULT.ERR_CLOSED + if not self.can_read: return FRESULT.ERR_PERM + bytes_read: ULONG = 0 + result: BOOL = win32.win32file.ReadFile(self.handle, buf, count, &bytes_read, None) + if result == 0: return FRESULT.ERR_IO + return LONG(bytes_read) + + def write(self, buf: bytes, count: ULONG) -> LONG: + if self.closed: return FRESULT.ERR_CLOSED + if not self.can_write: return FRESULT.ERR_PERM + bytes_written: ULONG = 0 + result: BOOL = win32.win32file.WriteFile(self.handle, buf, count, &bytes_written, None) + if result == 0: return FRESULT.ERR_IO + return LONG(bytes_written) + + def write_str(self, s: str) -> LONG: + if self.closed: return FRESULT.ERR_CLOSED + if not self.can_write: return FRESULT.ERR_PERM + length: ULONG = 0 + p: bytes = s + while *p != 0: + length += 1 + p += 1 + return self.write(s, length) + + def seek(self, offset: LONG, origin: ULONG) -> LONG: + if self.closed: return FRESULT.ERR_CLOSED + result: LONG = win32.win32file.SetFilePointer(self.handle, offset, None, origin) + if result == -1: return FRESULT.ERR_IO + return result + + def tell(self) -> LONG: + if self.closed: return FRESULT.ERR_CLOSED + return self.seek(0, SEEK_CUR) + + def close(self) -> LONG: + if self.closed: return FRESULT.ERR_CLOSED + win32.win32base.CloseHandle(self.handle) + self.closed = True + return FRESULT.OK + + def flush(self) -> LONG: + if self.closed: return FRESULT.ERR_CLOSED + win32.win32file.FlushFileBuffers(self.handle) + return FRESULT.OK + + def size(self) -> LONGLONG: + if self.closed: return -1 + li: win32.win32base.LARGE_INTEGER = win32.win32base.LARGE_INTEGER() + result: BOOL = win32.win32file.GetFileSizeEx(self.handle, &li) + if result == 0: return -1 + return li.QuadPart + + def readline(self, buf: bytes, max_count: ULONG) -> LONG: + if self.closed: return FRESULT.ERR_CLOSED + if not self.can_read: return FRESULT.ERR_PERM + if max_count < 2: return FRESULT.ERR + total: ULONG = 0 + bytes_read: ULONG = 0 + ch: int = 0 + while total < max_count - 1: + bytes_read = 0 + result: BOOL = win32.win32file.ReadFile(self.handle, buf + total, 1, &bytes_read, None) + if result == 0 or bytes_read == 0: break + # Explicit byte read: indexing loads 1 byte by the pointer element type (i8), same as Repl.vp _read_line. + # Do not use c.Deref(buf + total): its width depends on whether the pointer is i8*, but 1 byte is required here. + ch = buf[total] + total += 1 + if ch == 10: break + if total < max_count: + buf[total] = 0 + return LONG(total) + + def read_all(self, buf: bytes, buf_size: sizet) -> LONG: + """Read the whole file into buf as a C string (seeks to 0 first); returns bytes read, negative on error. + buf_size is capacity incl. terminator (sizet: same width as f.size(), so a 64-bit size is NOT + silently narrowed on the way in): reads at most buf_size-1 bytes then writes a NUL at buf[ret]. + A file longer than buf_size-1 is silently truncated; compare f.size() first to detect truncation. + A single ReadFile is capped at DWORD; a capacity above 4GiB-1 is treated as a failure here.""" + if self.closed: return FRESULT.ERR_CLOSED + if not self.can_read: return FRESULT.ERR_PERM + if buf_size < 2: return FRESULT.ERR + file_size: LONGLONG = self.size() + if file_size < 0: return FRESULT.ERR_IO + # 本次可读字节数 = min(file_size, buf_size-1):全程用 sizet 计算,只在交给 + # ReadFile(ULONG) 处显式窄化(单次读上限 DWORD,故超限直接失败,不静默截断)。 + to_read_sz: sizet = buf_size - 1 + if sizet(file_size) < to_read_sz: + to_read_sz = sizet(file_size) + if to_read_sz > 0xFFFFFFFF: return FRESULT.ERR + to_read: ULONG = ULONG(to_read_sz) + self.seek(0, SEEK_SET) + bytes_read: ULONG = 0 + result: BOOL = win32.win32file.ReadFile(self.handle, buf, to_read, &bytes_read, None) + if result == 0: return FRESULT.ERR_IO + if sizet(bytes_read) < buf_size: + buf[bytes_read] = 0 + return LONG(bytes_read) + + +def open(filename: str, mode: ULONG, share: ULONG = SHARE_READ) -> File*: + return new File(filename, mode, share) + +def openw(filename: LPCWSTR, mode: ULONG, share: ULONG = SHARE_READ) -> FileW*: + return new FileW(filename, mode, share)