Skip to content

Instantly share code, notes, and snippets.

@bruvzg
Last active October 4, 2024 18:04
Show Gist options
  • Save bruvzg/ee5a90d27856dbbd8c8561f81ee06bde to your computer and use it in GitHub Desktop.
Save bruvzg/ee5a90d27856dbbd8c8561f81ee06bde to your computer and use it in GitHub Desktop.
/***************************************************************************/
/* Godot PCK extractor (pckext.cpp) */
/***************************************************************************/
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program. If not, see <https://www.gnu.org/licenses/>. */
/***************************************************************************/
//Build command: g++ ./pckext.cpp -o pckext -lcrypto
#include <errno.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <openssl/md5.h>
int32_t read32(FILE *p_file) {
int32_t i32 = 0;
fread(&i32, sizeof(i32), 1, p_file);
return i32;
}
int64_t read64(FILE *p_file) {
int64_t i64 = 0;
fread(&i64, sizeof(i64), 1, p_file);
return i64;
}
void print_usage(const char *p_exename) {
printf("Godot PCK extractor.\n\n");
printf("Usage: %s [command] [file] [-d dir]\n\n", p_exename);
printf("Commands:\n");
printf(" -t test integrity of archive\n");
printf(" -l list contents of archive\n");
printf(" -x extract files (with full paths)\n");
printf("Options:\n");
printf(" -d <DIR> output directory\n");
}
void make_tree(const char *p_path) {
int path_len = strlen(p_path);
for (int i = 1; i < path_len; ++i) {
if (p_path[i] == '/') {
char *dir_buffer = (char *)calloc(i, 1);
strncpy(dir_buffer, p_path, i);
#if defined(_WIN32)
mkdir(dir_buffer);
#else
mkdir(dir_buffer, 0700);
#endif
free(dir_buffer);
}
}
}
bool test_file(uint64_t p_offset, uint64_t p_size, FILE *p_archive, uint8_t p_md5[16]) {
MD5_CTX ctx;
MD5_Init(&ctx);
fpos_t old_pos;
fgetpos(p_archive, &old_pos);
fseek(p_archive, p_offset, SEEK_SET);
void *buffer = calloc(1, 65536);
int blk = p_size / 65536;
size_t read = 0;
for (int i = 0; i <= blk; i++) {
size_t _size = (i == blk) ? p_size % 65536 : 65536;
size_t _read = fread(buffer, 1, _size, p_archive);
read += _read;
MD5_Update(&ctx, (unsigned char *)buffer, _read);
}
free(buffer);
fsetpos(p_archive, &old_pos);
uint8_t md5[MD5_DIGEST_LENGTH];
MD5_Final(md5, &ctx);
if (p_size != read) {
printf(", size mismatch - %lu (should be %llu)", read, p_size);
}
bool ok = true;
for (int i = 0; i < 16; i++) {
ok &= (md5[i] == p_md5[i]);
}
if (!ok) {
printf(", digest mismatch - ");
for (int i = 0; i < 16; i++) {
printf("%02x", md5[i]);
}
printf(" (should be ");
for (int i = 0; i < 16; i++) {
printf("%02x", p_md5[i]);
}
printf(")");
} else {
printf(", digest OK");
}
return ok;
}
bool extract_file(const char* p_filename, uint64_t p_offset, uint64_t p_size, FILE *p_archive, uint8_t p_md5[16]) {
//Make directory tree
make_tree(p_filename);
MD5_CTX ctx;
MD5_Init(&ctx);
//Extract file
FILE *ext_file = fopen(p_filename, "wb");
if (ext_file == NULL) {
printf(", could not open %s.", p_filename);
return false;
}
fpos_t old_pos;
fgetpos(p_archive, &old_pos);
fseek(p_archive, p_offset, SEEK_SET);
void *buffer = calloc(1, 65536);
int blk = p_size / 65536;
size_t read = 0;
size_t writ = 0;
for (int i = 0; i <= blk; i++) {
size_t _size = (i == blk) ? p_size % 65536 : 65536;
size_t _read = fread(buffer, 1, _size, p_archive);
read += _read;
MD5_Update(&ctx, (unsigned char *)buffer, _read);
writ += fwrite(buffer, 1, _read, ext_file);
}
free(buffer);
uint8_t md5[MD5_DIGEST_LENGTH];
MD5_Final(md5, &ctx);
if ((read != writ) || (p_size != writ)) {
printf(", size mismatch - %lu (should be %llu)", read, p_size);
}
bool ok = true;
for (int i = 0; i < 16; i++) {
ok &= (md5[i] == p_md5[i]);
}
if (!ok) {
printf(", digest mismatch - ");
for (int i = 0; i < 16; i++) {
printf("%02x", md5[i]);
}
printf(" (should be ");
for (int i = 0; i < 16; i++) {
printf("%02x", p_md5[i]);
}
printf(")");
}
fsetpos(p_archive, &old_pos);
fclose(ext_file);
return ok;
}
int main(int argc, char *argv[]) {
if ((argc != 3) && (argc != 5)) {
print_usage(argv[0]);
return -1;
}
if ((strcmp(argv[1], "-t") != 0) && (strcmp(argv[1], "-l") != 0) && (strcmp(argv[1], "-x") != 0)) {
printf("Invalid command: %s\n\n", argv[1]);
print_usage(argv[0]);
return -1;
}
if ((argc == 5) && ((strcmp(argv[3], "-d") != 0) || (strcmp(argv[1], "-x") != 0))) {
printf("Invalid option: %s\n\n", argv[3]);
print_usage(argv[0]);
return -1;
}
FILE *file = fopen(argv[2], "rb");
if (file == NULL) {
printf("Could not open %s\n", argv[2]);
return -1;
}
//Change current dir to output
if ((argc == 5) && (strcmp(argv[3], "-d") == 0)) {
if (chdir(argv[4]) != 0) {
printf("Could not open output directory.\n");
fclose(file);
return -1;
}
}
//Read magic @ find offset for self contained exe
int32_t magic = read32(file);
bool is_embedded = false;
if (magic != 0x43504447) {
fseek(file, -4, SEEK_END);
magic = read32(file);
if (magic != 0x43504447) {
printf("Invalid file format.\n");
fclose(file);
return -1;
}
fseek(file, -12, SEEK_CUR);
uint64_t ds = read64(file);
fseek(file, -(ds + 8), SEEK_CUR);
magic = read32(file);
if (magic != 0x43504447) {
printf("Invalid file format.\n");
fclose(file);
return -1;
}
bool is_embedded = true;
}
//Read version
uint32_t version = read32(file);
uint32_t ver_major = read32(file);
uint32_t ver_minor = read32(file);
uint32_t ver_rev = read32(file);
//Reserved
uint32_t extra[16];
for (int i = 0; i < 16; i++) {
extra[i] = read32(file);
}
printf("PCK version: %d, created by Godot %d.%d rev %d, %s\n\n", version, ver_major, ver_minor, ver_rev, is_embedded ? "self contained exe" : "standalone");
if ((version != 0) && (version != 1)) {
printf("Unsupported PCK version!\n");
fclose(file);
return -1;
}
//Read file list
int file_count = read32(file);
bool valid = true;
for (int i = 0; i < file_count; i++) {
uint32_t name_size = read32(file);
char *name_buffer = (char *)calloc(name_size + 1, 1);
memset(name_buffer, 0, name_size + 1);
fread(name_buffer, name_size, 1, file);
uint64_t ofs = read64(file);
uint64_t size = read64(file);
uint8_t md5[16];
fread(md5, 16, 1, file);
if (strcmp(argv[1], "-t") == 0) {
printf(" testing: %s", name_buffer + 6); //skip res://
valid &= test_file(ofs, size, file, md5);
printf("\n");
} else if (strcmp(argv[1], "-x") == 0) {
printf(" extracting: %s", name_buffer + 6); //skip res://
valid &= extract_file(name_buffer + 6, ofs, size, file, md5); //skip res://
printf("\n");
} else {
printf(" %s (%llu bytes)\n", name_buffer + 6, size); //skip res://
}
free(name_buffer);
}
fclose(file);
if ((strcmp(argv[1], "-t") == 0) || (strcmp(argv[1], "-x") == 0)) {
if (valid) {
printf("\nNo errors detected.\n");
} else {
printf("\nAt least one error was detected!\n");
return -1;
}
}
return 0;
}
@girng
Copy link

girng commented Jan 2, 2019

ty for the share. unfortunately i'm getting

pckextractor.cpp: In function ‘bool test_file(uint64_t, uint64_t, FILE*, uint8_t*)’:
pckextractor.cpp:91:64: warning: format ‘%llu’ expects argument of type ‘long long unsigned int’, but argument 3 has type ‘uint64_t {aka long unsigned int}’ [-Wformat=]
   printf(", size mismatch - %lu (should be %llu)", read, p_size);
                                                                ^
pckextractor.cpp: In function ‘bool extract_file(const char*, uint64_t, uint64_t, FILE*, uint8_t*)’:
pckextractor.cpp:151:64: warning: format ‘%llu’ expects argument of type ‘long long unsigned int’, but argument 3 has type ‘uint64_t {aka long unsigned int}’ [-Wformat=]
   printf(", size mismatch - %lu (should be %llu)", read, p_size);
                                                                ^
pckextractor.cpp: In function ‘int main(int, char**)’:
pckextractor.cpp:277:55: warning: format ‘%llu’ expects argument of type ‘long long unsigned int’, but argument 3 has type ‘uint64_t {aka long unsigned int}’ [-Wformat=]
    printf("  %s (%llu bytes)\n", name_buffer + 6, size);  //skip res://
                                                       ^
/tmp/ccE2Q1wl.o: In function `test_file(unsigned long, unsigned long, _IO_FILE*, unsigned char*)':
pckextractor.cpp:(.text+0x1ee): undefined reference to `MD5_Init'
pckextractor.cpp:(.text+0x2ed): undefined reference to `MD5_Update'
pckextractor.cpp:(.text+0x334): undefined reference to `MD5_Final'
/tmp/ccE2Q1wl.o: In function `extract_file(char const*, unsigned long, unsigned long, _IO_FILE*, unsigned char*)':
pckextractor.cpp:(.text+0x509): undefined reference to `MD5_Init'
pckextractor.cpp:(.text+0x65b): undefined reference to `MD5_Update'
pckextractor.cpp:(.text+0x6b2): undefined reference to `MD5_Final'
collect2: error: ld returned 1 exit status

my cmd is: g++ -std=c++11 pckextractor.cpp -o pckextractor && ./pckextractor

@girng
Copy link

girng commented Jan 2, 2019

@monkeyx-net
Copy link

This compiled for me on Manjaro
g++ -std=c++11 pckext.cpp -lcrypto -o pckextractor && ./pckextractor

@zaddok
Copy link

zaddok commented Aug 16, 2024

For OSX:

brew install openssl
g++ -std=c++11 pckext.cpp -L/opt/homebrew/opt/openssl/lib/ -I/opt/homebrew/opt/openssl/include/ -lcrypto -o godot_pck_extract

Although it doesn't seem to work with pck files created with Godot 4.

./godot_pck_extract -l myapp-3.0.pck
PCK version: 2, created by Godot 4.2 rev 2, standalone

Unsupported PCK version!

Is it an easy fix to get v2 (Godot 4??) support? reddit discussion

@bruvzg
Copy link
Author

bruvzg commented Aug 16, 2024

Is it an easy fix to get v2 (Godot 4??) support?

You can use something like this (if it's not encrypted PCK):

--- a.cpp	2024-08-16 07:56:39
+++ b.cpp	2024-08-16 08:10:56
@@ -241,6 +241,19 @@
     uint32_t ver_major = read32(file);
     uint32_t ver_minor = read32(file);
     uint32_t ver_rev = read32(file);
+    uint64_t file_base = 0;
+    uint32_t flags = 0;
+    
+    if (version == 2) {
+	flags = read32(file);
+	file_base = read64(file);
+	
+	if (flags & 1) {
+	    printf("Encrypted PCK!");
+	    fclose(file);
+	    return -1;
+	}
+    }
 
     //Reserved
     uint32_t extra[16];
\ No newline at end of file
@@ -250,7 +263,7 @@
 
     printf("PCK version: %d, created by Godot %d.%d rev %d, %s\n\n", version, ver_major, ver_minor, ver_rev, is_embedded ? "self contained exe" : "standalone");
 
-    if ((version != 0) && (version != 1)) {
+    if ((version != 0) && (version != 1) && (version != 2)) {
 	printf("Unsupported PCK version!\n");
 	fclose(file);
 	return -1;
\ No newline at end of file
@@ -266,12 +279,19 @@
 	memset(name_buffer, 0, name_size + 1);
 	fread(name_buffer, name_size, 1, file);
 
-	uint64_t ofs = read64(file);
+	uint64_t ofs = read64(file) + file_base;
 	uint64_t size = read64(file);
+	uint32_t file_flags = 0;
 	uint8_t md5[16];
 	fread(md5, 16, 1, file);
-
-	if (strcmp(argv[1], "-t") == 0) {
+	if (version == 2) {
+	    file_flags = read32(file);
+	}
+	
+	if (file_flags & 1) {
+	    printf("  %s (%llu bytes), encrypted file, skipped\n", name_buffer + 6, size);
+	    valid = false;
+	} else if (strcmp(argv[1], "-t") == 0) {
 	    printf("  testing: %s", name_buffer + 6);  //skip res://
 	    valid &= test_file(ofs, size, file, md5);
 	    printf("\n");
\ No newline at end of file

Or you can use https://github.com/bruvzg/gdsdecomp

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment