refactor(fileio): read_all 容量形参 ULONG → sizet(File/FileW)
与 f.size()(LONGLONG)同宽,消除'64 位尺寸进 32 位形参'的隐式窄化诱因;内部 min(file_size, cap-1) 全程用 sizet,只在交给 ReadFile(ULONG) 处显式窄化,且 cap-1 > 4GiB-1 直接返回失败(单次读上限 DWORD),不再静默回绕。
This commit is contained in:
1 parent
466c777f34
commit
1c9447dc82
1 file changed
+449
+449
@@ -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)
|
||||
Reference in new issue
Block a user