-
-
Save liamwhite/ba39ce769424b53a5505 to your computer and use it in GitHub Desktop.
Celestia's ARK
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* celestias-ark.c | |
* | |
* This file is part of Twilight's Trick. | |
* | |
* Twilight's Trick 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. | |
* | |
* Twilight's Trick 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 <http://www.gnu.org/licenses/>. | |
*/ | |
/* To build: gcc celestias-ark.c -o ark -lzstd */ | |
#include <assert.h> /* assert() */ | |
#include <errno.h> | |
#include <stdint.h> /* uint32_t */ | |
#include <stdlib.h> | |
#include <stdio.h> | |
#include <string.h> | |
#include <sys/stat.h> /* mkdir() */ | |
#include <sys/time.h> /* utimes() */ | |
#include <unistd.h> /* chdir() */ | |
#include <zstd.h> /* ZSTD_decompress() */ | |
/* "Shared secret" known by all copies of the Android Gameloft game */ | |
static uint32_t KEY[4] = {0x3d5b2a34, 0x923fff10, 0x00e346a4, 0x0c74902b}; | |
typedef struct _ark_header_t { | |
uint32_t file_count; | |
uint32_t metadata_offset; | |
uint32_t ark_version; | |
} ark_header_t; | |
typedef struct _ark_file_metadata_t { | |
uint8_t filename[128]; /* (ASCII) name of the file */ | |
uint8_t pathname[128]; /* (ASCII) name of the containing directory */ | |
uint32_t file_location; /* fseek() to this position from the start of the ARK */ | |
uint32_t original_filesize; /* unpacked buffer size */ | |
uint32_t compressed_size; /* compressed size, equal to original_filesize if no compression */ | |
uint32_t encrypted_nbytes; /* encrypted data size from XXTEA block cipher, zero if plain */ | |
uint32_t timestamp; /* Unix timestamp of file */ | |
uint32_t md5sum[4]; /* md5sum of original file */ | |
uint32_t priority; /* Default is 0, these will never be extracted to disk by the game; | |
Items with priority 1 will be cached to disk instead. */ | |
} ark_file_metadata_t; | |
/** | |
* Convert bytes to XXTEA n-bytes. | |
*/ | |
static uint32_t get_xxtea_phdr_size(uint32_t phdr_off); | |
/** | |
* Read data from @a stream into @a hdr. | |
*/ | |
static int parse_ark_header(ark_header_t *hdr, FILE *stream); | |
/** | |
* The caller must free the returned memory. | |
*/ | |
static ark_file_metadata_t *retrieve_ark_metadata(ark_header_t *hdr, FILE *stream); | |
/** | |
* Retrieve decrypted and decompressed data from the file referred to by @a meta. | |
* The caller must free the returned memory. | |
*/ | |
static void *dump_file_data(ark_file_metadata_t const *meta, FILE *stream); | |
/** | |
* Decrypt @a srclen words of data in @a src using the Corrected Block TEA cipher. | |
*/ | |
static void xxtea_decrypt(void *src, uint32_t srclen, uint32_t const key[4]); | |
int main(int argc, char *argv[]) | |
{ | |
FILE *file, *stream; | |
ark_file_metadata_t *meta, *meta_ptr; | |
void *data; | |
size_t i = 0; | |
ark_header_t header; | |
assert(sizeof(ark_file_metadata_t) == 296); | |
/* Verify arguments */ | |
if (argc != 2 || strcmp(argv[1], "--help") == 0) { | |
fprintf(stderr, "Celestia's ARK\nCopyright (C) 2014-2018 Liam P. White\n"); | |
fprintf(stderr, "This is free software: you are free to change and redistribute it.\n\n"); | |
fprintf(stderr, "Usage: %s path/to/arkfile.ark\n", argv[0]); | |
return 1; | |
} | |
file = fopen(argv[1], "rb"); | |
if (file == NULL) { | |
fprintf(stderr, "Failed to fopen() path: %s", argv[1]); | |
return -1; | |
} | |
if (parse_ark_header(&header, file) == 1) { | |
printf("Number of files in ARK: %u\n", header.file_count); | |
printf("Location of file table: %u\n", header.metadata_offset); | |
printf("ARK version number: %u\n", header.ark_version); | |
assert(header.ark_version == 3); | |
} else { | |
fprintf(stderr, "Error parsing ARK header!\n"); | |
return -1; | |
} | |
meta_ptr = meta = retrieve_ark_metadata(&header, file); | |
if (meta == NULL) { | |
fprintf(stderr, "Error retrieving ARK metadata. Are you sure this is actually an ARK file?\n"); | |
return -1; | |
} | |
printf("File list:\n"); | |
for (; i < header.file_count; ++i) { | |
printf("%s ", meta->filename); | |
printf("(length: %u)\n", meta->original_filesize); | |
data = dump_file_data(meta, file); | |
/* Write the output file | |
Note: this is a hack but it works pretty well */ | |
if (*meta->pathname != '\0') { | |
if (mkdir(meta->pathname, 0777) == 0 || errno == EEXIST) { | |
assert(chdir(meta->pathname) == 0); | |
} else { | |
perror("mkdir"); | |
assert(0); /* crash */ | |
} | |
} | |
stream = fopen(meta->filename, "wb"); | |
assert(stream != NULL); | |
fwrite(data, meta->original_filesize, 1, stream); | |
fclose(stream); | |
/* Set corresponding timestamp */ | |
struct timeval tv[2]; | |
tv[0].tv_sec = meta->timestamp; | |
tv[0].tv_usec = 0; | |
tv[1] = tv[0]; /* why bother doing it again? */ | |
utimes(meta->filename, tv); | |
/* Go back to parent */ | |
if (*meta->pathname != '\0') { | |
assert(chdir("../") == 0); | |
} | |
free(data); | |
meta++; | |
} | |
printf("\n\nDone!\n"); | |
free(meta_ptr); | |
return 0; | |
} | |
static uint32_t get_xxtea_phdr_size(uint32_t phdr_off) | |
{ | |
if (phdr_off & 3) { | |
phdr_off &= ~3u; | |
phdr_off += 4; | |
} | |
return phdr_off; | |
} | |
static int parse_ark_header(ark_header_t *hdr, FILE *stream) | |
{ | |
assert(hdr != NULL); | |
return fread(hdr, sizeof(ark_header_t), 1, stream) == 1; | |
} | |
static ark_file_metadata_t *retrieve_ark_metadata(ark_header_t *hdr, FILE *stream) | |
{ | |
uint32_t filesize; | |
if (fseek(stream, 0, SEEK_END) != 0) { | |
return NULL; | |
} | |
filesize = ftell(stream); | |
if (filesize < 0) { | |
return NULL; | |
} | |
uint32_t metadata_size = get_xxtea_phdr_size(filesize - hdr->metadata_offset); | |
uint32_t raw_metadata_size = hdr->file_count * sizeof(ark_file_metadata_t); | |
void *metadata = malloc(metadata_size); | |
if (fseek(stream, hdr->metadata_offset, SEEK_SET) != 0) { | |
free(metadata); | |
return NULL; | |
} | |
if (fread(metadata, metadata_size, 1, stream) != 1) { | |
free(metadata); | |
return NULL; | |
} | |
xxtea_decrypt(metadata, metadata_size / 4, KEY); | |
// Decompress | |
void *raw_metadata = malloc(raw_metadata_size); | |
ZSTD_decompress(raw_metadata, raw_metadata_size, metadata, metadata_size); | |
free(metadata); | |
return raw_metadata; | |
} | |
static void *dump_file_data(ark_file_metadata_t const *metadata, FILE *stream) | |
{ | |
fseek(stream, metadata->file_location, SEEK_SET); | |
void *file_data = malloc(metadata->encrypted_nbytes ? metadata->encrypted_nbytes : metadata->original_filesize); | |
fread(file_data, metadata->encrypted_nbytes ? metadata->encrypted_nbytes : metadata->compressed_size, 1, stream); | |
/* File was encrypted */ | |
if (metadata->encrypted_nbytes != 0) { | |
xxtea_decrypt(file_data, metadata->encrypted_nbytes / 4, KEY); | |
} | |
/* File was compressed */ | |
if (metadata->compressed_size != metadata->original_filesize) { | |
size_t destlen = metadata->original_filesize; | |
void *uncompressed_data = malloc(destlen); | |
assert(ZSTD_decompress(uncompressed_data, destlen, file_data, metadata->compressed_size) == destlen); | |
free(file_data); | |
file_data = uncompressed_data; | |
} | |
return file_data; | |
} | |
#define DELTA 0x9e3779b9 | |
#define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z))) | |
static void xxtea_decrypt(void *src, uint32_t n, uint32_t const key[4]) | |
{ | |
uint32_t *v = (uint32_t *)src; | |
uint32_t y, z, sum; | |
uint32_t p, rounds, e; | |
rounds = 6 + 52/n; | |
sum = rounds * DELTA; | |
y = v[0]; | |
do { | |
e = (sum >> 2) & 3; | |
for (p = n - 1; p > 0; p--) { | |
z = v[p-1]; | |
y = v[p] -= MX; | |
} | |
z = v[n - 1]; | |
y = v[0] -= MX; | |
sum -= DELTA; | |
} while (--rounds); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment