#!/usr/bin/env python3
"""List the files packed inside a Godot .pck, or fail if any match a pattern.
tools/pck-ls.py <file.pck> # list every packed path + size
tools/pck-ls.py <file.pck> --forbid '*.dll' # exit 1 if any packed path matches
Why this exists: the Windows preset once packed bin/windows/*.dll INTO the .pck.
Godot's FileAccess::exists() then found the packed copy of the relative path
bin/windows/<dll>, so OS_Windows::open_dynamic_library skipped its exe-dir
fallback and LoadLibraryExW was handed a file that exists only inside the
archive -> "Error 126" for every GDExtension (v0.1207.0). A string grep of the
.pck cannot tell a packed file from a path merely named in a config file; the
directory can, so this reads it.
Format (core/io/file_access_pack.cpp): 'GDPC', u32 format version, u32 major,
minor, patch; v2+: u32 pack flags, u64 file base; v3+: u64 directory offset;
16 reserved u32; then (at the directory offset for v3+) u32 file count and per
file: u32 path length, path (NUL-padded), u64 offset, u64 size, 16-byte md5,
u32 file flags. Encrypted directories (pack flag bit 0) are not supported.
"""
import fnmatch
import struct
import sys
PACK_DIR_ENCRYPTED = 1
def read_dir(path):
with open(path, 'rb') as f:
if f.read(4) != b'GDPC':
raise SystemExit(f'{path}: not a Godot .pck (no GDPC magic)')
fmt, major, minor, patch = struct.unpack('<4I', f.read(16))
flags = 0
dir_offset = None
if fmt >= 2:
flags, _file_base = struct.unpack('<IQ', f.read(12))
if fmt >= 3:
dir_offset, = struct.unpack('<Q', f.read(8))
f.read(16 * 4)
if flags & PACK_DIR_ENCRYPTED:
raise SystemExit(f'{path}: encrypted pack directory, cannot list')
if dir_offset is not None:
f.seek(dir_offset)
count, = struct.unpack('<I', f.read(4))
files = []
for _ in range(count):
n, = struct.unpack('<I', f.read(4))
name = f.read(n).rstrip(b'\0').decode('utf-8')
_offset, size = struct.unpack('<QQ', f.read(16))
f.read(16 + 4)
files.append((name, size))
return (fmt, major, minor, patch), files
def main(argv):
if len(argv) < 2:
raise SystemExit(__doc__)
pck = argv[1]
forbid = []
i = 2
while i < len(argv):
if argv[i] == '--forbid' and i + 1 < len(argv):
forbid.append(argv[i + 1])
i += 2
else:
raise SystemExit(f'unknown argument: {argv[i]}')
(fmt, major, minor, patch), files = read_dir(pck)
if not forbid:
print(f'# {pck}: pck format {fmt}, godot {major}.{minor}.{patch}, {len(files)} files')
for name, size in files:
print(f'{size:>12} {name}')
return 0
hits = [(n, s) for n, s in files
if any(fnmatch.fnmatch(n.removeprefix('res://'), p) for p in forbid)]
for name, size in hits:
print(f'FORBIDDEN in {pck}: {name} ({size} bytes)')
print(f'{pck}: {len(files)} files checked against {forbid}: {len(hits)} forbidden')
return 1 if hits else 0
if __name__ == '__main__':
sys.exit(main(sys.argv))