mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
498 lines
12 KiB
C++
498 lines
12 KiB
C++
/*
|
|
* Copyright (C) 2005 Taylor Richards
|
|
*
|
|
*/
|
|
|
|
|
|
|
|
#include "cfile/cfile.h"
|
|
#include "globalincs/pstypes.h"
|
|
|
|
#include <vector>
|
|
#include <iostream>
|
|
#include <cstdlib>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
|
|
#ifdef _WIN32
|
|
#include <direct.h>
|
|
#else
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <errno.h>
|
|
|
|
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// GLOBAL FUNCTIONS AND VARIABLES
|
|
//
|
|
|
|
// right out of pstypes.h but we don't want to use the INTEL_INT macro here
|
|
// since it would require SDL which isn't used on WIN32 platforms
|
|
#if BYTE_ORDER == BIG_ENDIAN
|
|
#define INT_SWAP(x) ( \
|
|
(x << 24) | \
|
|
(((ulong)x) >> 24) | \
|
|
((x & 0x0000ff00) << 8) | \
|
|
((x & 0x00ff0000) >> 8) \
|
|
)
|
|
#else
|
|
#define INT_SWAP(x) (x)
|
|
#endif
|
|
|
|
FILE *fp_in = NULL;
|
|
FILE *fp_out = NULL;
|
|
|
|
#ifndef MAX_PATH
|
|
#define MAX_PATH 255
|
|
#endif
|
|
|
|
char out_dir[MAX_PATH];
|
|
|
|
#define BLOCK_SIZE (1024*1024)
|
|
|
|
char tmp_data[BLOCK_SIZE]; // 1 MB
|
|
|
|
static int have_index = 0;
|
|
static int have_header = 0;
|
|
|
|
#define ERR_NO_FP_IN 0
|
|
#define ERR_NO_FP_OUT 1
|
|
#define ERR_INVALID_VP 2
|
|
#define ERR_NO_INDEX 3
|
|
#define ERR_NO_HEADER 4
|
|
#define ERR_PATH_TOO_LONG 5
|
|
|
|
typedef struct vp_header {
|
|
char id[4]; // 'VPVP'
|
|
int version;
|
|
int index_offset; // offset where the file index starts, also a total file data size count
|
|
int num_files; // number of files, including each step in the directory structure
|
|
} vp_header;
|
|
|
|
vp_header VP_Header;
|
|
|
|
typedef struct vp_fileinfo {
|
|
// stuff that is actually written to the VP
|
|
int offset; // offset of where this file is in the VP
|
|
int file_size; // size of file
|
|
char file_name[CF_MAX_FILENAME_LENGTH]; // filename
|
|
_fs_time_t write_time; // date/time in _fs_time_t type (a 32-bit version of time_t)
|
|
|
|
// not in the VP
|
|
char file_path[CF_MAX_PATHNAME_LENGTH]; // file path, generated here and not actually in the VP on a per file basis
|
|
|
|
vp_fileinfo() {
|
|
offset = 0;
|
|
file_size = 0;
|
|
file_name[0] = '\0';
|
|
file_path[0] = '\0';
|
|
write_time = 0;
|
|
}
|
|
|
|
~vp_fileinfo() {}
|
|
} vp_fileinfo;
|
|
|
|
SCP_vector<vp_fileinfo> VP_FileInfo;
|
|
|
|
// just like strlwr() but without actually including it here
|
|
void lowercase(char *s)
|
|
{
|
|
if (s == NULL)
|
|
return;
|
|
|
|
while (*s) {
|
|
*s = tolower(*s);
|
|
s++;
|
|
}
|
|
}
|
|
|
|
//
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
|
|
// /////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// The useful stuff...
|
|
//
|
|
|
|
void print_error(int err)
|
|
{
|
|
switch (err) {
|
|
case ERR_NO_FP_IN:
|
|
printf("ERROR: The input file (fp_in) is NULL! Exiting...\n");
|
|
break;
|
|
case ERR_NO_FP_OUT:
|
|
printf("ERROR: The output file (fp_out) is NULL! Exiting...\n");
|
|
break;
|
|
case ERR_INVALID_VP:
|
|
printf("ERROR: The specified VP file is invalid! Exiting...\n");
|
|
break;
|
|
case ERR_NO_INDEX:
|
|
printf("ERROR: No file index is available! Exiting...\n");
|
|
break;
|
|
case ERR_NO_HEADER:
|
|
printf("ERROR: No file header is available! Exiting...\n");
|
|
break;
|
|
case ERR_PATH_TOO_LONG:
|
|
printf("ERROR: Path to output directory is too long! Exiting...\n");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
exit(1);
|
|
}
|
|
|
|
void read_header()
|
|
{
|
|
fseek(fp_in, 0, SEEK_SET);
|
|
fread(&VP_Header.id, 1, 4, fp_in);
|
|
fread(&VP_Header.version, 1, sizeof(int), fp_in);
|
|
fread(&VP_Header.index_offset, 1, sizeof(int), fp_in);
|
|
fread(&VP_Header.num_files, 1, sizeof(int), fp_in);
|
|
|
|
VP_Header.version = INT_SWAP(VP_Header.version);
|
|
VP_Header.index_offset = INT_SWAP(VP_Header.index_offset);
|
|
VP_Header.num_files = INT_SWAP(VP_Header.num_files);
|
|
|
|
// check for a valid id (VPVP)
|
|
if ( (VP_Header.id[0] != 'V') && (VP_Header.id[1] != 'P') && (VP_Header.id[2] != 'V') && (VP_Header.id[3] != 'P') )
|
|
print_error(ERR_INVALID_VP);
|
|
|
|
// the index_offset needs to be greater than the size of vp_header at the very least and there should be at least one file
|
|
if ( !(VP_Header.index_offset > (int)sizeof(vp_header)) || !(VP_Header.num_files > 1) )
|
|
print_error(ERR_INVALID_VP);
|
|
|
|
|
|
have_header = 1;
|
|
}
|
|
|
|
void read_index(int lc = 0)
|
|
{
|
|
if (fp_in == NULL)
|
|
print_error(ERR_NO_FP_IN);
|
|
else if (!have_header)
|
|
print_error(ERR_NO_HEADER);
|
|
|
|
int i;
|
|
char path[CF_MAX_PATHNAME_LENGTH];
|
|
|
|
fseek(fp_in, VP_Header.index_offset, SEEK_SET);
|
|
|
|
memset(path, 0, CF_MAX_PATHNAME_LENGTH);
|
|
|
|
for ( i = 0; i < VP_Header.num_files; i++) {
|
|
vp_fileinfo vpinfo;
|
|
|
|
fread(&vpinfo.offset, 1, sizeof(int), fp_in);
|
|
fread(&vpinfo.file_size, 1, sizeof(int), fp_in);
|
|
fread(&vpinfo.file_name, 1, CF_MAX_FILENAME_LENGTH, fp_in);
|
|
fread(&vpinfo.write_time, 1, sizeof(_fs_time_t), fp_in);
|
|
|
|
vpinfo.offset = INT_SWAP(vpinfo.offset);
|
|
vpinfo.file_size = INT_SWAP(vpinfo.file_size);
|
|
vpinfo.write_time = INT_SWAP(vpinfo.write_time);
|
|
|
|
// check if it's a directory and if so then create a path to use for files
|
|
if (vpinfo.file_size == 0) {
|
|
// if we get a ".." then drop down in the path
|
|
if (!strcmp(vpinfo.file_name, "..")) {
|
|
char *s = strrchr(path, DIR_SEPARATOR_CHAR);
|
|
|
|
if (s)
|
|
*s = '\0';
|
|
|
|
// skip, we don't want to add the ".." to the path
|
|
continue;
|
|
}
|
|
|
|
// don't add the very first directory separator character, easier to clean later
|
|
if (path[0] != 0)
|
|
strcat( path, DIR_SEPARATOR_STR );
|
|
|
|
// always make directory names lower case for ease of use
|
|
lowercase( vpinfo.file_name );
|
|
|
|
strcat( path, vpinfo.file_name );
|
|
} // it's a file then
|
|
else {
|
|
// lowercase the filename if wanted
|
|
if (lc == 1)
|
|
lowercase( vpinfo.file_name );
|
|
|
|
strcpy_s( vpinfo.file_path, path );
|
|
VP_FileInfo.push_back(vpinfo);
|
|
}
|
|
}
|
|
|
|
have_index = 1;
|
|
}
|
|
|
|
void extract_all_files(char *file)
|
|
{
|
|
if (fp_in == NULL)
|
|
print_error(ERR_NO_FP_IN);
|
|
else if (!have_header)
|
|
print_error(ERR_NO_HEADER);
|
|
else if (!have_index)
|
|
print_error(ERR_NO_INDEX);
|
|
|
|
int status, m_error, nbytes, nbytes_remaining;
|
|
char path[CF_MAX_PATHNAME_LENGTH+CF_MAX_FILENAME_LENGTH+1]; // path length + filename length + extra NULL
|
|
char path2[CF_MAX_PATHNAME_LENGTH+CF_MAX_FILENAME_LENGTH+1]; // path length + filename length + extra NULL
|
|
char *c;
|
|
ubyte have_outdir = 0;
|
|
|
|
int out_len = strlen(out_dir);
|
|
|
|
if (out_len > 0)
|
|
have_outdir = 1;
|
|
|
|
printf("VP file extractor - version 0.6\n");
|
|
printf("\n");
|
|
|
|
if (have_outdir) {
|
|
printf("Output directory: \"%s\"\n", out_dir);
|
|
}
|
|
|
|
printf("Extracting: %s...\n", file);
|
|
|
|
for (size_t i = 0; i < VP_FileInfo.size(); i++) {
|
|
// save the file path to a temp location and recursively make the needed directories
|
|
if (have_outdir) {
|
|
if ( (out_len + 1 + strlen(path)) > MAX_PATH-1 )
|
|
print_error(ERR_PATH_TOO_LONG);
|
|
|
|
sprintf(path, "%s%s%s%s", out_dir, DIR_SEPARATOR_STR, VP_FileInfo[i].file_path, DIR_SEPARATOR_STR);
|
|
} else {
|
|
sprintf(path, "%s%s", VP_FileInfo[i].file_path, DIR_SEPARATOR_STR);
|
|
}
|
|
|
|
c = &path[0];
|
|
|
|
while (c++) {
|
|
c = strchr(c, DIR_SEPARATOR_CHAR);
|
|
|
|
if (c) {
|
|
*c = '\0'; // NULL at DIR_SEP char
|
|
|
|
#ifdef _WIN32
|
|
status = _mkdir(path);
|
|
#else
|
|
status = mkdir(path, 0777);
|
|
#endif
|
|
|
|
m_error = errno;
|
|
|
|
if (status && (m_error != EEXIST) ) {
|
|
printf("Cannot mkdir %s: %s\n", VP_FileInfo[i].file_path, strerror(m_error));
|
|
continue;
|
|
}
|
|
|
|
*c = DIR_SEPARATOR_CHAR; // replace DIR_SEP char
|
|
}
|
|
}
|
|
|
|
memset( path, 0, CF_MAX_PATHNAME_LENGTH+CF_MAX_FILENAME_LENGTH+1);
|
|
|
|
// start writing out the file
|
|
fseek(fp_in, VP_FileInfo[i].offset, SEEK_SET);
|
|
|
|
sprintf(path, "%s%s%s", VP_FileInfo[i].file_path, DIR_SEPARATOR_STR, VP_FileInfo[i].file_name);
|
|
|
|
// this is cheap, I know.
|
|
if (have_outdir) {
|
|
if ( (out_len + 1 + strlen(path)) > MAX_PATH-1 )
|
|
print_error(ERR_PATH_TOO_LONG);
|
|
|
|
sprintf(path2, "%s%s%s", out_dir, DIR_SEPARATOR_STR, path);
|
|
} else {
|
|
strcpy_s(path2, path);
|
|
}
|
|
|
|
fp_out = fopen(path2, "wb");
|
|
|
|
printf(" %s ... ", path);
|
|
|
|
if (fp_out == NULL) {
|
|
printf("can't create file!\n");
|
|
continue;
|
|
}
|
|
|
|
nbytes_remaining = VP_FileInfo[i].file_size;
|
|
|
|
while ( nbytes_remaining > 0 ) {
|
|
nbytes = fread( tmp_data, 1, MIN(BLOCK_SIZE, nbytes_remaining), fp_in );
|
|
|
|
if (nbytes > 0) {
|
|
fwrite( tmp_data, 1, nbytes, fp_out );
|
|
nbytes_remaining -= nbytes;
|
|
}
|
|
}
|
|
|
|
printf("done!\n");
|
|
|
|
fclose(fp_out);
|
|
fp_out = NULL;
|
|
}
|
|
|
|
printf("\n");
|
|
}
|
|
|
|
// TODO: the formatting of this is a bit loco but I don't care enough to make it better...
|
|
void list_all_files(char *file)
|
|
{
|
|
char out_time[20];
|
|
float one_k = 1024.0f;
|
|
float one_m = 1048576.0f;
|
|
char m;
|
|
float div_by;
|
|
time_t plat_time;
|
|
|
|
// if we don't have an index then bail
|
|
if (!have_index)
|
|
print_error(ERR_NO_INDEX);
|
|
|
|
|
|
printf("VP file extractor - version 0.6\n");
|
|
printf("\n");
|
|
printf("%s:\n", file);
|
|
printf("\n");
|
|
|
|
printf(" Name Size Offset Date/Time Path\n");
|
|
printf("-------------------------------------------------------------------------------\n");
|
|
for (size_t i = 0; i < VP_FileInfo.size(); i++) {
|
|
plat_time = VP_FileInfo[i].write_time; // gets rid of some platform strangeness this way
|
|
strftime(out_time, 32, "%F %H:%M", localtime(&plat_time)); // YYYY-mm-dd HH:mm (ISO 8601 date format, 24-hr time)
|
|
|
|
if (VP_FileInfo[i].file_size > (int)one_m) {
|
|
m = 'M';
|
|
div_by = one_m;
|
|
} else {
|
|
m = 'K';
|
|
div_by = one_k;
|
|
}
|
|
|
|
int len = strlen( VP_FileInfo[i].file_name );
|
|
printf("%s %*.1f%c %10i %18s %3s%s\n", VP_FileInfo[i].file_name, 33 - len, (float)VP_FileInfo[i].file_size/div_by, m, VP_FileInfo[i].offset, out_time, DIR_SEPARATOR_STR, VP_FileInfo[i].file_path);
|
|
}
|
|
|
|
printf("\n");
|
|
|
|
// we use the vector size here since VP_Header.num_files would include each entry
|
|
// in the directory tree as well as individual files and that artificially inflates
|
|
// the number of files that we show or would extract
|
|
printf("Total files: %i\n", (int)VP_FileInfo.size());
|
|
|
|
// yeah, I'm just that cheap
|
|
if (VP_Header.index_offset > (int)one_m) {
|
|
m = 'M';
|
|
div_by = one_m;
|
|
} else {
|
|
m = 'K';
|
|
div_by = one_k;
|
|
}
|
|
|
|
printf("Total size: %.1f%c\n", (float)VP_Header.index_offset/div_by, m);
|
|
|
|
printf("\n");
|
|
}
|
|
|
|
void help()
|
|
{
|
|
printf("VP file extractor - version 0.6\n");
|
|
printf("\n");
|
|
printf("Usage: cfileextractor [-x | -l] [-L] [-o <dir>] <vp_filename>\n");
|
|
printf("\n");
|
|
printf(" Commands (only one at the time):\n");
|
|
printf(" -x | --extract Extract all files into current directory.\n");
|
|
printf(" -l | --list List all files in VP archive.\n");
|
|
printf(" -h | --help Show this help text.\n");
|
|
printf("\n");
|
|
printf(" Options:\n");
|
|
printf(" -L | --lowercase Force all filenames to be lower case.\n");
|
|
printf(" -o <dir> Extract files to <dir> instead of current directory.\n");
|
|
printf(" NOTE: No spaces allowed, enclose path in \" \" if needed.\n");
|
|
printf("\n");
|
|
printf(" (No command specified will list all files in the VP archive.)\n");
|
|
printf("\n");
|
|
}
|
|
|
|
// we end up #include'ing SDL.h which on Windows and Mac will redefine main() which is something
|
|
// that we don't want since we don't actually link against SDL, this solves the problem...
|
|
#ifdef main
|
|
#undef main
|
|
#endif
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
int extract = 0, lc = 0, list = 0;
|
|
|
|
if (argc < 2) {
|
|
help();
|
|
return 0;
|
|
}
|
|
|
|
if (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
|
|
help();
|
|
return 0;
|
|
}
|
|
|
|
// the last option should always be the VP file that we want
|
|
fp_in = fopen(argv[argc-1], "rb");
|
|
|
|
if (fp_in == NULL)
|
|
print_error(ERR_NO_FP_IN);
|
|
|
|
// if the header is invalid then read_header() should exit us out
|
|
read_header();
|
|
|
|
memset(out_dir, 0, MAX_PATH);
|
|
|
|
// TODO: add filter based extraction
|
|
for (int i = 1; i < argc-1; i++) {
|
|
if (!strcmp(argv[i], "-x") || !strcmp(argv[i], "--extract")) {
|
|
if (list) {
|
|
help();
|
|
exit(0);
|
|
}
|
|
extract = 1;
|
|
} else if (!strcmp(argv[i], "-L") || !strcmp(argv[i], "--lowercase")) {
|
|
lc = 1;
|
|
} else if (!strcmp(argv[i], "-l") || !strcmp(argv[i], "--list")) {
|
|
if (extract) {
|
|
help();
|
|
exit(0);
|
|
}
|
|
list = 1;
|
|
} else if ( !strcmp(argv[i], "-o") && (i+1 < argc) && (argv[i+1][0] != '-') ) {
|
|
strncpy(out_dir, argv[i+1], MAX_PATH-1);
|
|
i++; // have to increment "i" past the output directory
|
|
} else {
|
|
help();
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// read the file index, make all filenames lowercase if wanted
|
|
read_index( lc );
|
|
|
|
if (extract) {
|
|
extract_all_files( argv[argc-1] );
|
|
return 0;
|
|
} else if (list) {
|
|
list_all_files( argv[argc-1] );
|
|
return 0;
|
|
} else {
|
|
// if only -L is specified or no command is specified then just list files
|
|
list_all_files( argv[argc-1] );
|
|
return 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|