Files
Viper_VRTE/zipfile.vp
T

623 lines
22 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
# zipfile.vp — ZIP 归档读写模块(Python zipfile 子集)
#
# 支持:
# - 写入成员: ZIP_STORED / ZIP_DEFLATED(raw deflate,wbits=-15,CRC32 校验)
# - 读取成员: 自动识别 ZIP_STORED / ZIP_DEFLATED,解压后做 CRC32 校验
# - 读取时从文件尾部回扫 EOCD(容忍注释/前导数据)
#
# 不支持: zip64(>4GB/成员数>65535)、加密、分卷、数据描述符(flag bit 3)等。
#
# 成员名为原始 UTF-8 字节(写入时置 flag bit 11,读取时不自动解码)。
#
# 用法(对齐 Python 语义,只读模式自动把整个 zip 读入内存):
# w: rawptr[zipfile.ZipFile] = zipfile.ZipFile("out.zip", "w")
# w.write("a.txt", buf, buf_len) # 默认 ZIP_DEFLATED
# w.close()
#
# r: rawptr[zipfile.ZipFile] = zipfile.ZipFile("out.zip", "r")
# olen: sizet = 0
# data: bytes = r.read("a.txt", &olen) # malloc 缓冲,用后 free
# err = zipfile.error_code() / zipfile.error()
import t, c
import stdlib
import string
import viperlib
import w32.win32base as wb
import w32.win32file as wf
import zlib.pyzlib as zpy
# ============================================================
# constants
# ============================================================
define ZIP_STORED = 0
define ZIP_DEFLATED = 8
define ZIP_LEVEL_DEFAULT = 6 # compression level (zlib default 6)
define _ZIP_FLAG_UTF8 = 0x0800 # filename in UTF-8 (flag bit 11)
define _ZIP_VERSION = 20 # version needed 2.0
define _ZIP_DOS_DATE_EPOCH = 0x21 # 1980-01-01
define _ZIP_MAX_COMMENT = 65535
# error codes (method return values; for global last error see error_code()/error())
define ZIP_ERR_OK = 0
define ZIP_ERR_OPEN = -1
define ZIP_ERR_NOT_ZIP = -2
define ZIP_ERR_MEMORY = -3
define ZIP_ERR_NOT_FOUND = -4
define ZIP_ERR_INVALID = -5
define ZIP_ERR_UNSUPPORTED = -6
define ZIP_ERR_CRC = -7
define ZIP_ERR_IO = -8
define ZIP_ERR_MODE = -9
define ZIP_ERR_CLOSED = -10
# ============================================================
# module-level error state
# ============================================================
zipfile_error_code_val: int = 0
zipfile_error_msg: [256]char = ""
def zipfile_set_error(code: int, msg: str):
global zipfile_error_code_val, zipfile_error_msg
zipfile_error_code_val = code
if msg:
string.strncpy(zipfile_error_msg, msg, 255)
zipfile_error_msg[255] = '\0'
else:
zipfile_error_msg[0] = '\0'
def zipfile_set_error_win(code: int, msg: str, err: ulong):
global zipfile_error_code_val, zipfile_error_msg
zipfile_error_code_val = code
if err != 0:
viperlib.snprintf(zipfile_error_msg, 256, "%s (win32 err=%u)", msg, err)
else:
string.strncpy(zipfile_error_msg, msg, 255)
zipfile_error_msg[255] = '\0'
def error_code() -> int:
return zipfile_error_code_val
def error() -> str:
return zipfile_error_msg
# ============================================================
# small utilities
# ============================================================
def zf_strlen(s: bytes) -> sizet:
n: sizet = 0
while s is not None and c.Deref(s) != 0:
n += 1
s += 1
return n
def zf_dup_name(src: bytes, length: sizet) -> bytes:
buf: bytes = bytes(stdlib.malloc(length + 1))
if not buf: return None
if length > 0: string.memcpy(buf, src, length)
buf[length] = 0
return buf
def zf_u16le(p: uint8*) -> uint32:
return uint32(p[0]) | (uint32(p[1]) << 8)
def zf_u32le(p: uint8*) -> uint32:
return (uint32(p[0]) | (uint32(p[1]) << 8) | (uint32(p[2]) << 16) | (uint32(p[3]) << 24))
def zf_w16le(h: uint8*, v: uint32):
h[0] = v & 0xFF
h[1] = (v >> 8) & 0xFF
def zf_w32le(h: uint8*, v: uint32):
h[0] = v & 0xFF
h[1] = (v >> 8) & 0xFF
h[2] = (v >> 16) & 0xFF
h[3] = (v >> 24) & 0xFF
# All file openings allow read/write/delete sharing, avoiding mutually exclusive handles
define _ZF_SHARE = 7 # FILE_SHARE_READ|WRITE|DELETE
def zf_share_flags() -> ulong:
return ulong(_ZF_SHARE)
def zf_write_all(h: wb.HANDLE, p: uint8*, n: sizet) -> int:
done: sizet = 0
while done < n:
chunk: sizet = n - done
if chunk > 0xFFFFFF00:
chunk = 0xFFFFFF00
written: ulong = 0
ok: int = wf.WriteFile(h, p + done, ulong(chunk), &written, None)
if ok == 0 or written == 0:
global zipfile_error_code_val, zipfile_error_msg
zipfile_error_code_val = ZIP_ERR_IO
viperlib.snprintf(zipfile_error_msg, 256, "write failed (win32_err=%u)", wb.GetLastError())
return ZIP_ERR_IO
done += sizet(written)
return ZIP_ERR_OK
def zf_read_all(h: wb.HANDLE, p: uint8*, n: sizet) -> int:
done: sizet = 0
while done < n:
chunk: sizet = n - done
if chunk > 0xFFFFFF00:
chunk = 0xFFFFFF00
bytes_read: ulong = 0
ok: int = wf.ReadFile(h, p + done, ulong(chunk), c.Addr(bytes_read), None)
if ok == 0 or bytes_read == 0:
global zipfile_error_code_val, zipfile_error_msg
zipfile_error_code_val = ZIP_ERR_IO
viperlib.snprintf(zipfile_error_msg, 256,
"read failed (win32_err=%u)", wb.GetLastError())
return ZIP_ERR_IO
done += sizet(bytes_read)
return ZIP_ERR_OK
def zf_read_whole(path: str, out_len: rawptr[sizet]) -> uint8*:
if not out_len:
zipfile_set_error(ZIP_ERR_OPEN, "out_len is None")
return None
h: wb.HANDLE = wf.CreateFileA(
path, wf.GENERIC_READ, zf_share_flags(),
None, wf.OPEN_EXISTING, wf.FILE_ATTRIBUTE_NORMAL, None)
if h == wb.INVALID_HANDLE_VALUE:
zipfile_set_error_win(ZIP_ERR_OPEN, "cannot open file for reading", wb.GetLastError())
return None
li: wb.LARGE_INTEGER = wb.LARGE_INTEGER()
ok_size: int = wf.GetFileSizeEx(h, t.CPtr(c.Addr(li)))
if ok_size == 0:
wb.CloseHandle(h)
zipfile_set_error(ZIP_ERR_IO, "cannot get file size")
return None
sz: sizet = sizet(li.QuadPart)
if sz == 0:
wb.CloseHandle(h)
zipfile_set_error(ZIP_ERR_NOT_ZIP, "empty file is not a zip archive")
return None
buf: bytes = bytes(stdlib.malloc(sz))
if not buf:
wb.CloseHandle(h)
zipfile_set_error(ZIP_ERR_MEMORY, "out of memory")
return None
rc: int = zf_read_all(h, buf, sz)
wb.CloseHandle(h)
if rc != ZIP_ERR_OK:
stdlib.free(buf)
return None
*out_len = sz
return buf
# ============================================================
# 条目信息(目录项)
# 注意: 节点为显式 calloc 分配
# 由 ZipFile 负责释放——避免 GC 实例经 | t.CPtr 联合字段引用被回收。
# ============================================================
class ZipInfo:
filename: bytes # 原始字节名(含 \0,zipfile 持有并负责释放)
name_len: sizet
compress_type: int # ZIP_STORED / ZIP_DEFLATED
crc32: uint32
compress_size: sizet
file_size: sizet
header_offset: sizet # 本地文件头偏移
next: rawptr[ZipInfo] # 链表(保持写入/目录顺序)
# ============================================================
# ZipFile
# ============================================================
class ZipFile:
path: bytes
mode: int = -1 # 0 = 读,1 = 写
compression: int # write() 未显式指定时的压缩类型
fh: wb.HANDLE = None # 写模式句柄(裸 win32 CreateFileA)
data: bytes = None # 读模式:整个归档的内存副本
data_len: sizet = 0
info_head: rawptr[ZipInfo] = None
info_tail: rawptr[ZipInfo] = None
info_count: sizet = 0
pos: sizet = 0 # 写模式当前偏移
ok: int = 0 # 1 = 可正常使用
closed: int = 0
def __init__(self, path: str, mode: str = "r", compression: int = ZIP_DEFLATED):
self.path = path
self.compression = compression
# 注意: 字符串必须用 strcmp 比较(== 只会比较指针)
if string.strcmp(mode, "r") == 0: self.mode = 0
elif string.strcmp(mode, "w") == 0: self.mode = 1
else:
zipfile_set_error(ZIP_ERR_OPEN, "mode must be 'r' or 'w'")
self.closed = 1
return
if self.mode == 0:
self.data = zf_read_whole(path, &self.data_len)
if not self.data:
self.closed = 1
return
if self._parse() != ZIP_ERR_OK:
stdlib.free(self.data)
self.data = None
self.data_len = 0
self._clear_infos()
self.closed = 1
return
else:
self.fh = wf.CreateFileA(
path, wf.GENERIC_WRITE, zf_share_flags(),
None, wf.CREATE_ALWAYS, wf.FILE_ATTRIBUTE_NORMAL, None)
if self.fh == wb.INVALID_HANDLE_VALUE:
zipfile_set_error_win(ZIP_ERR_OPEN, "cannot create file for writing", wb.GetLastError())
self.fh = None
self.closed = 1
return
self.pos = 0
self.ok = 1
def _clear_infos(self):
n: rawptr[ZipInfo] = self.info_head
while n is not None:
if n.filename is not None:
stdlib.free(n.filename)
n.filename = None
nxt: rawptr[ZipInfo] = n.next
n.next = None
stdlib.free(n)
n = nxt
self.info_head = None
self.info_tail = None
self.info_count = 0
def _append_info(self) -> rawptr[ZipInfo]:
node: rawptr[ZipInfo] = stdlib.calloc(1, ZipInfo.__sizeof__())
if not node:
zipfile_set_error(ZIP_ERR_MEMORY, "out of memory")
return None
node.next = None
if self.info_tail is None:
self.info_head = node
else:
self.info_tail.next = node
self.info_tail = node
self.info_count += 1
return node
# ---- 读模式:解析中央目录 ----
def _parse(self) -> int:
if self.data_len < 22:
zipfile_set_error(ZIP_ERR_NOT_ZIP, "file too small to be a zip")
return ZIP_ERR_NOT_ZIP
d: bytes = self.data
# 从尾部回扫 EOCD(容忍尾部注释)
max_back: sizet = self.data_len - 22
if max_back > _ZIP_MAX_COMMENT + 20:
max_back = _ZIP_MAX_COMMENT + 20
eocd: sizet = 0
found: int = 0
i: sizet = 0
while i <= max_back and found == 0:
p: sizet = self.data_len - 22 - i
if d[p] == 0x50 and d[p + 1] == 0x4B and d[p + 2] == 0x05 and d[p + 3] == 0x06:
eocd = p
found = 1
i += 1
if found == 0:
zipfile_set_error(ZIP_ERR_NOT_ZIP, "EOCD not found")
return ZIP_ERR_NOT_ZIP
total: uint32 = zf_u16le(d + eocd + 10)
cd_off: uint32 = zf_u32le(d + eocd + 16)
cd_size: uint32 = zf_u32le(d + eocd + 12)
if sizet(cd_off) + sizet(cd_size) > self.data_len:
zipfile_set_error(ZIP_ERR_NOT_ZIP, "central directory out of range")
return ZIP_ERR_NOT_ZIP
cp: sizet = sizet(cd_off)
end: sizet = sizet(cd_off) + sizet(cd_size)
idx: sizet = 0
while idx < total:
if cp + 46 > end:
zipfile_set_error(ZIP_ERR_NOT_ZIP, "central directory entry truncated")
return ZIP_ERR_NOT_ZIP
if zf_u32le(d + cp) != 0x02014B50:
zipfile_set_error(ZIP_ERR_NOT_ZIP, "bad central directory signature")
return ZIP_ERR_NOT_ZIP
method: uint32 = zf_u16le(d + cp + 10)
crc: uint32 = zf_u32le(d + cp + 16)
csize: uint32 = zf_u32le(d + cp + 20)
usize: uint32 = zf_u32le(d + cp + 24)
nlen: uint32 = zf_u16le(d + cp + 28)
elen: uint32 = zf_u16le(d + cp + 30)
clen: uint32 = zf_u16le(d + cp + 32)
hoff: uint32 = zf_u32le(d + cp + 42)
rec_len: sizet = 46 + sizet(nlen) + sizet(elen) + sizet(clen)
if cp + rec_len > end:
zipfile_set_error(ZIP_ERR_NOT_ZIP, "central directory entry out of range")
return ZIP_ERR_NOT_ZIP
if nlen == 0:
zipfile_set_error(ZIP_ERR_NOT_ZIP, "central directory entry has empty name")
return ZIP_ERR_NOT_ZIP
dup: bytes = zf_dup_name(d + cp + 46, sizet(nlen))
if not dup:
zipfile_set_error(ZIP_ERR_MEMORY, "out of memory")
return ZIP_ERR_MEMORY
node: rawptr[ZipInfo] = self._append_info()
if node is None:
stdlib.free(dup)
return ZIP_ERR_MEMORY
node.filename = dup
node.name_len = sizet(nlen)
node.compress_type = int(method)
node.crc32 = crc
node.compress_size = sizet(csize)
node.file_size = sizet(usize)
node.header_offset = sizet(hoff)
cp = cp + rec_len
idx += 1
return ZIP_ERR_OK
# ---- 公共查询 ----
def namelist_at(self, i: sizet) -> bytes:
if self.ok == 0 or self.info_count == 0 or i >= self.info_count:
return None
n: rawptr[ZipInfo] = self.info_head
k: sizet = 0
while n is not None:
if k == i:
return n.filename
n = n.next
k += 1
return None
def getinfo(self, name: str) -> rawptr[ZipInfo]:
if self.ok == 0:
return None
n: rawptr[ZipInfo] = self.info_head
while n is not None:
if n.filename is not None and string.strcmp(n.filename, name) == 0:
return n
n = n.next
return None
# ---- 读成员:解压 + CRC32 校验 ----
def read(self, name: str, out_len: sizet*) -> bytes:
if not out_len:
zipfile_set_error(ZIP_ERR_INVALID, "out_len is None")
return None
*out_len = 0
if self.closed or self.ok == 0:
zipfile_set_error(ZIP_ERR_CLOSED, "zip file is closed")
return None
if self.mode != 0:
zipfile_set_error(ZIP_ERR_MODE, "zip file is not open for reading")
return None
info: rawptr[ZipInfo] = self.getinfo(name)
if info is None:
zipfile_set_error(ZIP_ERR_NOT_FOUND, "member not found")
return None
d: bytes = self.data
off: sizet = sizet(info.header_offset)
if off + 30 > self.data_len:
zipfile_set_error(ZIP_ERR_NOT_ZIP, "local file header out of range")
return None
if zf_u32le(d + off) != 0x04034B50:
zipfile_set_error(ZIP_ERR_NOT_ZIP, "bad local file header signature")
return None
nlen: uint32 = zf_u16le(d + off + 26)
elen: uint32 = zf_u16le(d + off + 28)
start: sizet = off + 30 + sizet(nlen) + sizet(elen)
csz: sizet = sizet(info.compress_size)
if start + csz > self.data_len:
zipfile_set_error(ZIP_ERR_NOT_ZIP, "member data out of range")
return None
payload: bytes = d + start
out: bytes = None
ctype: int = info.compress_type
if ctype == ZIP_STORED:
out = bytes(stdlib.malloc(sizet(info.file_size) + 1))
if not out:
zipfile_set_error(ZIP_ERR_MEMORY, "out of memory")
return None
if csz > 0:
string.memcpy(out, payload, csz)
*out_len = sizet(info.file_size)
elif ctype == ZIP_DEFLATED:
olen: sizet = 0
out = zpy.decompress(None, payload, csz, -15, 0, &olen)
if not out:
zipfile_set_error(ZIP_ERR_IO, "deflate decompression failed")
return None
*out_len = olen
else:
zipfile_set_error(ZIP_ERR_UNSUPPORTED, "unsupported compression method")
return None
got_len: sizet = *out_len
if got_len != sizet(info.file_size):
stdlib.free(out)
*out_len = 0
zipfile_set_error(ZIP_ERR_NOT_ZIP, "member size mismatch")
return None
crc_calc: uint32 = uint32(zpy.zlib_crc32(out, got_len, 0))
if crc_calc != info.crc32:
stdlib.free(out)
*out_len = 0
zipfile_set_error(ZIP_ERR_CRC, "CRC32 mismatch")
return None
return out
# ---- 写成员 ----
def write(self, arcname: str, data: bytes, data_len: sizet,
compress_type: int = -1) -> int:
if self.closed or self.ok == 0:
zipfile_set_error(ZIP_ERR_CLOSED, "zip file is closed")
return ZIP_ERR_CLOSED
if self.mode != 1:
zipfile_set_error(ZIP_ERR_MODE, "zip file is not open for writing")
return ZIP_ERR_MODE
if not arcname:
zipfile_set_error(ZIP_ERR_INVALID, "arcname is required")
return ZIP_ERR_INVALID
ctype: int = compress_type
if ctype == -1:
ctype = self.compression
if ctype != ZIP_STORED and ctype != ZIP_DEFLATED:
zipfile_set_error(ZIP_ERR_UNSUPPORTED, "compress_type must be ZIP_STORED or ZIP_DEFLATED")
return ZIP_ERR_UNSUPPORTED
if data_len > 0 and not data:
zipfile_set_error(ZIP_ERR_INVALID, "data is None but data_len > 0")
return ZIP_ERR_INVALID
name_len: sizet = zf_strlen(arcname)
if name_len == 0:
zipfile_set_error(ZIP_ERR_INVALID, "arcname is empty")
return ZIP_ERR_INVALID
if name_len > 0xFFFF or data_len > 0xFFFFFFFF:
zipfile_set_error(ZIP_ERR_UNSUPPORTED, "name or data too large for zip32")
return ZIP_ERR_UNSUPPORTED
crc: uint32 = uint32(zpy.zlib_crc32(data, data_len, 0))
payload: bytes = data
plen: sizet = data_len
if ctype == ZIP_DEFLATED:
tmp: sizet = 0
payload = zpy.compress(None, data, data_len, ZIP_LEVEL_DEFAULT, -15, &tmp)
if not payload:
zipfile_set_error(ZIP_ERR_IO, "deflate compression failed")
return ZIP_ERR_IO
plen = tmp
hoff: sizet = self.pos
hdr: bytes = bytes(stdlib.malloc(30))
if not hdr:
if ctype == ZIP_DEFLATED:
stdlib.free(payload)
zipfile_set_error(ZIP_ERR_MEMORY, "out of memory")
return ZIP_ERR_MEMORY
string.memset(hdr, 0, 30)
zf_w32le(hdr, 0x04034B50)
zf_w16le(hdr + 4, _ZIP_VERSION)
zf_w16le(hdr + 6, _ZIP_FLAG_UTF8)
zf_w16le(hdr + 8, uint32(ctype))
zf_w16le(hdr + 12, _ZIP_DOS_DATE_EPOCH)
zf_w32le(hdr + 14, crc)
zf_w32le(hdr + 18, uint32(plen))
zf_w32le(hdr + 22, uint32(data_len))
zf_w16le(hdr + 26, uint32(name_len))
rc: int = zf_write_all(self.fh, hdr, 30)
stdlib.free(hdr)
if rc == ZIP_ERR_OK:
rc = zf_write_all(self.fh, arcname, name_len)
if rc == ZIP_ERR_OK:
rc = zf_write_all(self.fh, payload, plen)
if ctype == ZIP_DEFLATED:
stdlib.free(payload)
if rc != ZIP_ERR_OK:
return rc
self.pos = self.pos + 30 + name_len + plen
dup: bytes = zf_dup_name(arcname, name_len)
if not dup:
zipfile_set_error(ZIP_ERR_MEMORY, "out of memory")
return ZIP_ERR_MEMORY
node: rawptr[ZipInfo] = self._append_info()
if node is None:
stdlib.free(dup)
return ZIP_ERR_MEMORY
node.filename = dup
node.name_len = name_len
node.compress_type = ctype
node.crc32 = crc
node.compress_size = plen
node.file_size = data_len
node.header_offset = hoff
return ZIP_ERR_OK
# ---- 写模式收尾:中央目录 + EOCD ----
def _finish_write(self) -> int:
if self.info_count > _ZIP_MAX_COMMENT:
zipfile_set_error(ZIP_ERR_UNSUPPORTED, "too many entries (>65535)")
return ZIP_ERR_UNSUPPORTED
cd_start: sizet = self.pos
hdr: bytes = stdlib.malloc(46)
if not hdr:
zipfile_set_error(ZIP_ERR_MEMORY, "out of memory")
return ZIP_ERR_MEMORY
rc: int = ZIP_ERR_OK
n: rawptr[ZipInfo] = self.info_head
while n is not None and rc == ZIP_ERR_OK:
string.memset(hdr, 0, 46)
zf_w32le(hdr, 0x02014B50)
zf_w16le(hdr + 4, _ZIP_VERSION) # version made by
zf_w16le(hdr + 6, _ZIP_VERSION) # version needed
zf_w16le(hdr + 8, _ZIP_FLAG_UTF8)
zf_w16le(hdr + 10, uint32(n.compress_type))
zf_w16le(hdr + 14, _ZIP_DOS_DATE_EPOCH)
zf_w32le(hdr + 16, n.crc32)
zf_w32le(hdr + 20, uint32(n.compress_size))
zf_w32le(hdr + 24, uint32(n.file_size))
zf_w16le(hdr + 28, uint32(n.name_len))
zf_w32le(hdr + 42, uint32(n.header_offset))
rc = zf_write_all(self.fh, hdr, 46)
if rc == ZIP_ERR_OK:
rc = zf_write_all(self.fh, n.filename, n.name_len)
if rc == ZIP_ERR_OK:
self.pos = self.pos + 46 + n.name_len
n = n.next
stdlib.free(hdr)
if rc != ZIP_ERR_OK:
return rc
cd_size: sizet = self.pos - cd_start
e: bytes = stdlib.malloc(22)
if not e:
zipfile_set_error(ZIP_ERR_MEMORY, "out of memory")
return ZIP_ERR_MEMORY
string.memset(e, 0, 22)
zf_w32le(e, 0x06054B50)
zf_w16le(e + 8, uint32(self.info_count))
zf_w16le(e + 10, uint32(self.info_count))
zf_w32le(e + 12, uint32(cd_size))
zf_w32le(e + 16, uint32(cd_start))
rc = zf_write_all(self.fh, e, 22)
stdlib.free(e)
if rc != ZIP_ERR_OK: zipfile_set_error(ZIP_ERR_IO, "write EOCD failed")
return rc
def close(self) -> int:
if self.closed: return ZIP_ERR_OK
rc: int = ZIP_ERR_OK
if self.mode == 1 and self.fh is not None:
rc = self._finish_write()
wb.CloseHandle(self.fh)
self.fh = None
elif self.mode == 0:
if self.data is not None:
stdlib.free(self.data)
self.data = None
self.data_len = 0
self._clear_infos()
self.closed = 1
self.ok = 0
return rc