OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / tools / pck-ls.py

85 lines · python ref:
#!/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))