unpack_pkg.py 2.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. #!/usr/bin/env python3
  2. """Extracts files out of a Wallpaper Engine .pkg container (scene.pkg, general.pkg, etc).
  3. Usage:
  4. unpack_pkg.py <path-to.pkg> <output-dir> [name-filter ...]
  5. With no filters, everything is extracted. Filters are substring matches against
  6. the stored filename (case-insensitive), e.g.:
  7. unpack_pkg.py scene.pkg out scene.json
  8. unpack_pkg.py scene.pkg out models/spiritblossomahribase_puppet.mdl
  9. """
  10. import struct
  11. import sys
  12. import os
  13. def read_u32(f):
  14. return struct.unpack('<I', f.read(4))[0]
  15. def read_sized_string(f):
  16. n = read_u32(f)
  17. return f.read(n).decode('utf-8', errors='replace')
  18. def extract(pkg_path, out_dir, name_filters=None):
  19. with open(pkg_path, 'rb') as f:
  20. header = read_sized_string(f)
  21. if not header.startswith('PKGV'):
  22. raise ValueError(f"expected PKGV header, got {header!r}")
  23. count = read_u32(f)
  24. entries = []
  25. for _ in range(count):
  26. name = read_sized_string(f)
  27. offset = read_u32(f)
  28. length = read_u32(f)
  29. entries.append((name, offset, length))
  30. base = f.tell()
  31. os.makedirs(out_dir, exist_ok=True)
  32. for name, offset, length in entries:
  33. if name_filters and not any(want.lower() in name.lower() for want in name_filters):
  34. continue
  35. f.seek(base + offset)
  36. data = f.read(length)
  37. dest = os.path.join(out_dir, name.replace('\\', '/'))
  38. os.makedirs(os.path.dirname(dest), exist_ok=True)
  39. with open(dest, 'wb') as out:
  40. out.write(data)
  41. print(f"extracted {name} ({length} bytes)")
  42. return entries
  43. if __name__ == '__main__':
  44. if len(sys.argv) < 3:
  45. print(__doc__)
  46. sys.exit(1)
  47. pkg_path = sys.argv[1]
  48. out_dir = sys.argv[2]
  49. filters = sys.argv[3:] or None
  50. all_entries = extract(pkg_path, out_dir, filters)
  51. if filters is None:
  52. for entry_name, _, entry_length in all_entries:
  53. print(entry_name, entry_length)