diff --git a/CHANGELOG.md b/CHANGELOG.md index a283d08..f28c257 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Renamed the `fpack`/`funpack` CLI tools into a single `fpk` binary + taking `create`/`extract`/`list` subcommands (tar-style, with + `c`/`x`/`t` aliases). + ## [0.2.0] - 2026-09-16 ### Added diff --git a/README.md b/README.md index be1d1a1..3f7783e 100644 --- a/README.md +++ b/README.md @@ -14,17 +14,20 @@ build.bat debug release tools # Windows ## Usage ```bash -fpack [-o output.fpk] [-p] [-q] [-v] input_dir -funpack [-d output_dir] [-l] [-p] [-q] [-v] input.fpk +fpk create [-o output.fpk] [-p] [-q] [-v] input_dir +fpk extract [-d output_dir] [-p] [-q] [-v] input.fpk +fpk list input.fpk ``` -`fpack` walks `input_dir` recursively (skipping dotfiles) and writes an -archive. If `-o` is omitted, the output name is derived from the input -directory's basename plus `.fpk` (e.g. `assets/` -> `assets.fpk`). +`create` (alias `c`) walks `input_dir` recursively (skipping dotfiles) and +writes an archive. If `-o` is omitted, the output name is derived from the +input directory's basename plus `.fpk` (e.g. `assets/` -> `assets.fpk`). -`funpack` extracts (or, with `-l`, lists) the entries in `input.fpk`. If -`-d` is omitted, the output directory is derived from the archive's -basename with `.fpk` stripped (e.g. `assets.fpk` -> `assets/`). +`extract` (alias `x`) extracts the entries in `input.fpk`. If `-d` is +omitted, the output directory is derived from the archive's basename with +`.fpk` stripped (e.g. `assets.fpk` -> `assets/`). + +`list` (alias `t`) prints the entries in `input.fpk` without extracting. Common flags: `-p` show progress, `-q` suppress the summary line, `-v` print each entry as it's processed. diff --git a/config.mk b/config.mk index 2d4f15a..9ee3dc2 100644 --- a/config.mk +++ b/config.mk @@ -11,5 +11,4 @@ SRCS = \ src/fpk_dir.c TOOL_SRCS = \ - tools/fpack.c \ - tools/funpack.c + tools/fpk.c diff --git a/tools/fpack.c b/tools/fpack.c deleted file mode 100644 index 929fa58..0000000 --- a/tools/fpack.c +++ /dev/null @@ -1,260 +0,0 @@ -#if !defined(_WIN32) -#define _POSIX_C_SOURCE 200809L -#endif - -#include "fpk.h" -#include -#include -#include -#include -#include - -#ifdef _WIN32 -#include -#define PROGRAM_NAME "fpack.exe" -#else -#include -#define PROGRAM_NAME "fpack" -#endif - -static double now_seconds(void) -{ -#ifdef _WIN32 - LARGE_INTEGER freq, count; - - QueryPerformanceFrequency(&freq); - QueryPerformanceCounter(&count); - return (double)count.QuadPart / (double)freq.QuadPart; -#else - struct timespec ts; - - clock_gettime(CLOCK_MONOTONIC, &ts); - return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; -#endif -} - -static void format_duration(double secs, char *buf, size_t bufsize) -{ - unsigned long total_ms = (unsigned long)(secs * 1000.0 + 0.5); - unsigned long ms = total_ms % 1000; - unsigned long total_s = total_ms / 1000; - unsigned long s = total_s % 60; - unsigned long total_m = total_s / 60; - unsigned long m = total_m % 60; - unsigned long total_h = total_m / 60; - unsigned long h = total_h % 24; - unsigned long d = total_h / 24; - - if (d > 0) - sprintf(buf, "%lud %luh %lum %lus", d, h, m, s); - else if (h > 0) - sprintf(buf, "%luh %lum %lus", h, m, s); - else if (m > 0) - sprintf(buf, "%lum %lu.%03lus", m, s, ms); - else - sprintf(buf, "%lu.%03lus", s, ms); - buf[bufsize - 1] = '\0'; -} - -#define FPACK_QUIET (1u << 0) -#define FPACK_VERBOSE (1u << 1) -#define FPACK_PROGRESS (1u << 2) - -static unsigned char flags; -static uint64_t entry_count; -static uint64_t total_bytes; -static volatile sig_atomic_t interrupted; - -static void handle_sigint(int sig) -{ - (void)sig; - interrupted = 1; -} - -static struct option long_opts[] = { - {"output", required_argument, NULL, 'o'}, - {"progress", no_argument, NULL, 'p'}, - {"quiet", no_argument, NULL, 'q'}, - {"verbose", no_argument, NULL, 'v'}, - {"help", no_argument, NULL, 'h'}, - {NULL, 0, NULL, 0} -}; - -static char *dup_str(const char *s) -{ - size_t len = strlen(s) + 1; - char *out = malloc(len); - - memcpy(out, s, len); - return out; -} - -#ifdef _WIN32 -#define IS_PATH_SEP(c) ((c) == '/' || (c) == '\\') -#else -#define IS_PATH_SEP(c) ((c) == '/') -#endif - -static char *basename_dup(const char *path) -{ - size_t end = strlen(path); - size_t start; - char *out; - - while (end > 0 && IS_PATH_SEP(path[end - 1])) - end--; - - start = end; - while (start > 0 && !IS_PATH_SEP(path[start - 1])) - start--; - - out = malloc(end - start + 1); - memcpy(out, path + start, end - start); - out[end - start] = '\0'; - return out; -} - -static char *derive_output_name(const char *input_dir) -{ - char *base = basename_dup(input_dir); - size_t len = strlen(base); - char *out = malloc(len + 5); - - memcpy(out, base, len); - memcpy(out + len, ".fpk", 5); - free(base); - return out; -} - -static int on_add(const char *path, uint64_t size, void *ctx) -{ - (void)ctx; - - entry_count++; - total_bytes += size; - - if (flags & FPACK_VERBOSE) - printf(" %s (%lu bytes)\n", path, (unsigned long)size); - - if (flags & FPACK_PROGRESS) { - printf("\rpacked %lu files (%lu bytes)", - (unsigned long)entry_count, (unsigned long)total_bytes); - fflush(stdout); - } - - return !interrupted; -} - -static void usage(void) -{ - fprintf(stderr, "usage: %s [-o output.fpk] [-p] [-q] [-v] input_dir\n", PROGRAM_NAME); - fprintf(stderr, "try '%s --help' for more information\n", PROGRAM_NAME); - exit(1); -} - -static void help(void) -{ - printf("usage: %s [options] input_dir\n", PROGRAM_NAME); - printf("\n"); - printf("Pack a directory tree into an fpk archive.\n"); - printf("\n"); - printf("options:\n"); - printf(" -o, --output=FILE write the archive to FILE instead of the derived name\n"); - printf(" -p, --progress print packing progress\n"); - printf(" -q, --quiet suppress the final summary line\n"); - printf(" -v, --verbose print each packed entry\n"); - printf(" -h, --help show this help and exit\n"); - exit(0); -} - -int main(int argc, char **argv) -{ - const char *output_arg = NULL; - const char *input_dir; - char *output; - struct fpk_writer *w; - int c; - double start_time; - - while ((c = getopt_long(argc, argv, "o:pqvh", long_opts, NULL)) != -1) { - switch (c) { - case 'o': - output_arg = optarg; - break; - case 'p': - flags |= FPACK_PROGRESS; - break; - case 'q': - flags |= FPACK_QUIET; - break; - case 'v': - flags |= FPACK_VERBOSE; - break; - case 'h': - help(); - break; - default: - usage(); - } - } - - if (optind != argc - 1) - usage(); - - input_dir = argv[optind]; - output = output_arg ? dup_str(output_arg) : derive_output_name(input_dir); - - start_time = now_seconds(); - - w = fpk_writer_open(output); - if (!w) { - fprintf(stderr, "fpack: cannot create: %s\n", output); - free(output); - return 1; - } - - signal(SIGINT, handle_sigint); - - if (fpk_writer_add_dir(w, input_dir, on_add, NULL) != 0) { - fpk_writer_close(w); - if (interrupted) { - remove(output); - fprintf(stderr, "\nfpack: interrupted, removed incomplete archive: %s\n", output); - free(output); - return 130; - } - fprintf(stderr, "fpack: failed while packing: %s\n", input_dir); - free(output); - return 1; - } - - if (entry_count == 0) { - fprintf(stderr, "fpack: no files found in: %s\n", input_dir); - fpk_writer_close(w); - free(output); - return 1; - } - - if (fpk_writer_finalize(w) != 0) { - fprintf(stderr, "fpack: failed to finalize: %s\n", output); - fpk_writer_close(w); - free(output); - return 1; - } - - fpk_writer_close(w); - - if (flags & FPACK_PROGRESS) - printf("\n"); - - if (!(flags & FPACK_QUIET)) { - char dur[64]; - - format_duration(now_seconds() - start_time, dur, sizeof(dur)); - printf("wrote: %s (%lu entries) in %s\n", output, - (unsigned long)entry_count, dur); - } - - free(output); - return 0; -} diff --git a/tools/fpk.c b/tools/fpk.c new file mode 100644 index 0000000..bfcb81a --- /dev/null +++ b/tools/fpk.c @@ -0,0 +1,674 @@ +#if !defined(_WIN32) +#define _POSIX_C_SOURCE 200809L +#endif + +#include "fpk.h" +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#else +#include +#define MKDIR(path) mkdir(path, 0755) +#endif + +#ifdef _WIN32 +static wchar_t *utf8_to_wide(const char *s) +{ + int wlen = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0); + wchar_t *w; + + if (wlen <= 0) + return NULL; + w = malloc((size_t)wlen * sizeof(wchar_t)); + if (!w) + return NULL; + MultiByteToWideChar(CP_UTF8, 0, s, -1, w, wlen); + return w; +} + +static int win_mkdir(const char *path) +{ + wchar_t *wpath = utf8_to_wide(path); + int ok; + + if (!wpath) + return -1; + ok = CreateDirectoryW(wpath, NULL); + free(wpath); + if (ok) + return 0; + if (GetLastError() == ERROR_ALREADY_EXISTS) { + errno = EEXIST; + return -1; + } + errno = 0; + return -1; +} +#define MKDIR(path) win_mkdir(path) +#endif + +static double now_seconds(void) +{ +#ifdef _WIN32 + LARGE_INTEGER freq, count; + + QueryPerformanceFrequency(&freq); + QueryPerformanceCounter(&count); + return (double)count.QuadPart / (double)freq.QuadPart; +#else + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; +#endif +} + +static void format_duration(double secs, char *buf, size_t bufsize) +{ + unsigned long total_ms = (unsigned long)(secs * 1000.0 + 0.5); + unsigned long ms = total_ms % 1000; + unsigned long total_s = total_ms / 1000; + unsigned long s = total_s % 60; + unsigned long total_m = total_s / 60; + unsigned long m = total_m % 60; + unsigned long total_h = total_m / 60; + unsigned long h = total_h % 24; + unsigned long d = total_h / 24; + + if (d > 0) + sprintf(buf, "%lud %luh %lum %lus", d, h, m, s); + else if (h > 0) + sprintf(buf, "%luh %lum %lus", h, m, s); + else if (m > 0) + sprintf(buf, "%lum %lu.%03lus", m, s, ms); + else + sprintf(buf, "%lu.%03lus", s, ms); + buf[bufsize - 1] = '\0'; +} + +static char *dup_str(const char *s) +{ + size_t len = strlen(s) + 1; + char *out = malloc(len); + + memcpy(out, s, len); + return out; +} + +static char *join_path(const char *dir, const char *name) +{ + size_t dir_len = strlen(dir); + size_t name_len = strlen(name); + char *out = malloc(dir_len + 1 + name_len + 1); + + memcpy(out, dir, dir_len); + out[dir_len] = '/'; + memcpy(out + dir_len + 1, name, name_len); + out[dir_len + 1 + name_len] = '\0'; + return out; +} + +#ifdef _WIN32 +#define IS_PATH_SEP(c) ((c) == '/' || (c) == '\\') +#else +#define IS_PATH_SEP(c) ((c) == '/') +#endif + +static char *basename_dup(const char *path) +{ + size_t end = strlen(path); + size_t start; + char *out; + + while (end > 0 && IS_PATH_SEP(path[end - 1])) + end--; + + start = end; + while (start > 0 && !IS_PATH_SEP(path[start - 1])) + start--; + + out = malloc(end - start + 1); + memcpy(out, path + start, end - start); + out[end - start] = '\0'; + return out; +} + +static volatile sig_atomic_t interrupted; + +static void handle_sigint(int sig) +{ + (void)sig; + interrupted = 1; +} + +/* ---- create ---- */ + +#define CREATE_QUIET (1u << 0) +#define CREATE_VERBOSE (1u << 1) +#define CREATE_PROGRESS (1u << 2) + +static unsigned char create_flags; +static uint64_t create_entry_count; +static uint64_t create_total_bytes; + +static struct option create_long_opts[] = { + {"output", required_argument, NULL, 'o'}, + {"progress", no_argument, NULL, 'p'}, + {"quiet", no_argument, NULL, 'q'}, + {"verbose", no_argument, NULL, 'v'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0} +}; + +static char *derive_output_name(const char *input_dir) +{ + char *base = basename_dup(input_dir); + size_t len = strlen(base); + char *out = malloc(len + 5); + + memcpy(out, base, len); + memcpy(out + len, ".fpk", 5); + free(base); + return out; +} + +static int on_add(const char *path, uint64_t size, void *ctx) +{ + (void)ctx; + + create_entry_count++; + create_total_bytes += size; + + if (create_flags & CREATE_VERBOSE) + printf(" %s (%lu bytes)\n", path, (unsigned long)size); + + if (create_flags & CREATE_PROGRESS) { + printf("\rpacked %lu files (%lu bytes)", + (unsigned long)create_entry_count, (unsigned long)create_total_bytes); + fflush(stdout); + } + + return !interrupted; +} + +static void create_usage(void) +{ + fprintf(stderr, "usage: fpk create [-o output.fpk] [-p] [-q] [-v] input_dir\n"); + fprintf(stderr, "try 'fpk create --help' for more information\n"); + exit(1); +} + +static void create_help(void) +{ + printf("usage: fpk create [options] input_dir\n"); + printf("\n"); + printf("Pack a directory tree into an fpk archive.\n"); + printf("\n"); + printf("options:\n"); + printf(" -o, --output=FILE write the archive to FILE instead of the derived name\n"); + printf(" -p, --progress print packing progress\n"); + printf(" -q, --quiet suppress the final summary line\n"); + printf(" -v, --verbose print each packed entry\n"); + printf(" -h, --help show this help and exit\n"); + exit(0); +} + +static int cmd_create(int argc, char **argv) +{ + const char *output_arg = NULL; + const char *input_dir; + char *output; + struct fpk_writer *w; + int c; + double start_time; + + while ((c = getopt_long(argc, argv, "o:pqvh", create_long_opts, NULL)) != -1) { + switch (c) { + case 'o': + output_arg = optarg; + break; + case 'p': + create_flags |= CREATE_PROGRESS; + break; + case 'q': + create_flags |= CREATE_QUIET; + break; + case 'v': + create_flags |= CREATE_VERBOSE; + break; + case 'h': + create_help(); + break; + default: + create_usage(); + } + } + + if (optind != argc - 1) + create_usage(); + + input_dir = argv[optind]; + output = output_arg ? dup_str(output_arg) : derive_output_name(input_dir); + + start_time = now_seconds(); + + w = fpk_writer_open(output); + if (!w) { + fprintf(stderr, "fpk: cannot create: %s\n", output); + free(output); + return 1; + } + + signal(SIGINT, handle_sigint); + + if (fpk_writer_add_dir(w, input_dir, on_add, NULL) != 0) { + fpk_writer_close(w); + if (interrupted) { + remove(output); + fprintf(stderr, "\nfpk: interrupted, removed incomplete archive: %s\n", output); + free(output); + return 130; + } + fprintf(stderr, "fpk: failed while packing: %s\n", input_dir); + free(output); + return 1; + } + + if (create_entry_count == 0) { + fprintf(stderr, "fpk: no files found in: %s\n", input_dir); + fpk_writer_close(w); + free(output); + return 1; + } + + if (fpk_writer_finalize(w) != 0) { + fprintf(stderr, "fpk: failed to finalize: %s\n", output); + fpk_writer_close(w); + free(output); + return 1; + } + + fpk_writer_close(w); + + if (create_flags & CREATE_PROGRESS) + printf("\n"); + + if (!(create_flags & CREATE_QUIET)) { + char dur[64]; + + format_duration(now_seconds() - start_time, dur, sizeof(dur)); + printf("wrote: %s (%lu entries) in %s\n", output, + (unsigned long)create_entry_count, dur); + } + + free(output); + return 0; +} + +/* ---- extract ---- */ + +#define EXTRACT_PROGRESS (1u << 0) +#define EXTRACT_QUIET (1u << 1) +#define EXTRACT_VERBOSE (1u << 2) + +static unsigned char extract_flags; + +static struct option extract_long_opts[] = { + {"dir", required_argument, NULL, 'd'}, + {"progress", no_argument, NULL, 'p'}, + {"quiet", no_argument, NULL, 'q'}, + {"verbose", no_argument, NULL, 'v'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0} +}; + +static char *derive_output_dir(const char *input) +{ + char *base = basename_dup(input); + size_t len = strlen(base); + + if (len > 4 && strcmp(base + len - 4, ".fpk") == 0) + base[len - 4] = '\0'; + + return base; +} + +static int mkdir_p(const char *path) +{ + char *copy = dup_str(path); + char *p; + + for (p = copy + 1; *p; p++) { + if (*p != '/') + continue; + + *p = '\0'; + if (MKDIR(copy) != 0 && errno != EEXIST) { + fprintf(stderr, "fpk: cannot create directory: %s\n", copy); + free(copy); + return 0; + } + *p = '/'; + } + + if (MKDIR(copy) != 0 && errno != EEXIST) { + fprintf(stderr, "fpk: cannot create directory: %s\n", copy); + free(copy); + return 0; + } + + free(copy); + return 1; +} + +static int extract_entry(const char *out_dir, const fpk_entry_t *entry, + const void *data, size_t size) +{ + char *full_path = join_path(out_dir, entry->path); + FILE *fp; +#ifdef _WIN32 + wchar_t *wpath; +#endif + + if (strchr(entry->path, '/') != NULL) { + char *dir_part = dup_str(full_path); + char *slash = strrchr(dir_part, '/'); + + *slash = '\0'; + if (!mkdir_p(dir_part)) { + free(dir_part); + free(full_path); + return 0; + } + free(dir_part); + } + +#ifdef _WIN32 + wpath = utf8_to_wide(full_path); + fp = wpath ? _wfopen(wpath, L"wb") : NULL; + free(wpath); +#else + fp = fopen(full_path, "wb"); +#endif + if (!fp) { + fprintf(stderr, "fpk: cannot create: %s\n", full_path); + free(full_path); + return 0; + } + + if (fwrite(data, 1, size, fp) != size) { + fprintf(stderr, "fpk: write failed: %s\n", full_path); + fclose(fp); + free(full_path); + return 0; + } + + fclose(fp); + free(full_path); + return 1; +} + +static void extract_usage(void) +{ + fprintf(stderr, "usage: fpk extract [-d output_dir] [-p] [-q] [-v] input.fpk\n"); + fprintf(stderr, "try 'fpk extract --help' for more information\n"); + exit(1); +} + +static void extract_help(void) +{ + printf("usage: fpk extract [options] input.fpk\n"); + printf("\n"); + printf("Extract files from an fpk archive.\n"); + printf("\n"); + printf("options:\n"); + printf(" -d, --dir=DIR extract into DIR instead of the archive's derived name\n"); + printf(" -p, --progress print extraction progress\n"); + printf(" -q, --quiet suppress the final summary line\n"); + printf(" -v, --verbose print each extracted entry\n"); + printf(" -h, --help show this help and exit\n"); + exit(0); +} + +static int cmd_extract(int argc, char **argv) +{ + const char *dir_arg = NULL; + const char *input; + char *out_dir; + struct fpk *pak; + uint64_t count; + uint64_t i = 0; + int c; + double start_time; + + while ((c = getopt_long(argc, argv, "d:pqvh", extract_long_opts, NULL)) != -1) { + switch (c) { + case 'd': + dir_arg = optarg; + break; + case 'p': + extract_flags |= EXTRACT_PROGRESS; + break; + case 'q': + extract_flags |= EXTRACT_QUIET; + break; + case 'v': + extract_flags |= EXTRACT_VERBOSE; + break; + case 'h': + extract_help(); + break; + default: + extract_usage(); + } + } + + if (optind != argc - 1) + extract_usage(); + + input = argv[optind]; + + start_time = now_seconds(); + + pak = fpk_open(input); + if (!pak) { + fprintf(stderr, "fpk: cannot open: %s\n", input); + return 1; + } + + count = fpk_entry_count(pak); + + out_dir = dir_arg ? dup_str(dir_arg) : derive_output_dir(input); + + if (!mkdir_p(out_dir)) { + fpk_close(pak); + free(out_dir); + return 1; + } + + signal(SIGINT, handle_sigint); + + for (i = 0; i < count; i++) { + const fpk_entry_t *entry = fpk_entry_at(pak, i); + size_t size; + void *data; + + if (interrupted) + break; + + data = fpk_read(pak, entry, &size); + + if (!data) { + fprintf(stderr, "fpk: cannot read: %s\n", entry->path); + fpk_close(pak); + free(out_dir); + return 1; + } + + if (!extract_entry(out_dir, entry, data, size)) { + free(data); + fpk_close(pak); + free(out_dir); + return 1; + } + + if (extract_flags & EXTRACT_VERBOSE) + printf(" %s (%lu bytes)\n", entry->path, (unsigned long)size); + + if (extract_flags & EXTRACT_PROGRESS) { + printf("\runpacking %lu/%lu (%lu%%)", + (unsigned long)(i + 1), (unsigned long)count, + (unsigned long)(((i + 1) * 100) / count)); + fflush(stdout); + } + + free(data); + } + + fpk_close(pak); + + if ((extract_flags & EXTRACT_PROGRESS) && count > 0) + printf("\n"); + + if (interrupted) { + fprintf(stderr, "fpk: interrupted, %lu of %lu entries extracted\n", + (unsigned long)i, (unsigned long)count); + free(out_dir); + return 130; + } + + if (!(extract_flags & EXTRACT_QUIET)) { + char dur[64]; + + format_duration(now_seconds() - start_time, dur, sizeof(dur)); + printf("unpacked: %s -> %s (%lu entries) in %s\n", input, out_dir, + (unsigned long)count, dur); + } + + free(out_dir); + return 0; +} + +/* ---- list ---- */ + +static struct option list_long_opts[] = { + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0} +}; + +static void list_usage(void) +{ + fprintf(stderr, "usage: fpk list input.fpk\n"); + fprintf(stderr, "try 'fpk list --help' for more information\n"); + exit(1); +} + +static void list_help(void) +{ + printf("usage: fpk list input.fpk\n"); + printf("\n"); + printf("List the entries in an fpk archive.\n"); + printf("\n"); + printf("options:\n"); + printf(" -h, --help show this help and exit\n"); + exit(0); +} + +static int cmd_list(int argc, char **argv) +{ + const char *input; + struct fpk *pak; + uint64_t count; + uint64_t i; + int c; + + while ((c = getopt_long(argc, argv, "h", list_long_opts, NULL)) != -1) { + switch (c) { + case 'h': + list_help(); + break; + default: + list_usage(); + } + } + + if (optind != argc - 1) + list_usage(); + + input = argv[optind]; + + pak = fpk_open(input); + if (!pak) { + fprintf(stderr, "fpk: cannot open: %s\n", input); + return 1; + } + + count = fpk_entry_count(pak); + + for (i = 0; i < count; i++) { + const fpk_entry_t *entry = fpk_entry_at(pak, i); + + printf("%s\t%lu bytes\n", entry->path, + (unsigned long)entry->data_size); + } + + fpk_close(pak); + return 0; +} + +/* ---- top-level dispatch ---- */ + +static void top_usage(void) +{ + fprintf(stderr, "usage: fpk [options] ...\n"); + fprintf(stderr, "try 'fpk --help' for more information\n"); + exit(1); +} + +static void top_help(void) +{ + printf("usage: fpk [options] ...\n"); + printf("\n"); + printf("commands:\n"); + printf(" create, c pack a directory tree into an fpk archive\n"); + printf(" extract, x extract files from an fpk archive\n"); + printf(" list, t list the entries in an fpk archive\n"); + printf("\n"); + printf("run 'fpk --help' for command-specific options\n"); + exit(0); +} + +int main(int argc, char **argv) +{ + const char *verb; + + if (argc < 2) + top_usage(); + + verb = argv[1]; + + if (strcmp(verb, "-h") == 0 || strcmp(verb, "--help") == 0) + top_help(); + + if (strcmp(verb, "create") == 0 || strcmp(verb, "c") == 0) + return cmd_create(argc - 1, argv + 1); + + if (strcmp(verb, "extract") == 0 || strcmp(verb, "x") == 0) + return cmd_extract(argc - 1, argv + 1); + + if (strcmp(verb, "list") == 0 || strcmp(verb, "t") == 0) + return cmd_list(argc - 1, argv + 1); + + fprintf(stderr, "fpk: unknown command: %s\n", verb); + top_usage(); + return 1; +} diff --git a/tools/funpack.c b/tools/funpack.c deleted file mode 100644 index 63b7a13..0000000 --- a/tools/funpack.c +++ /dev/null @@ -1,412 +0,0 @@ -#if !defined(_WIN32) -#define _POSIX_C_SOURCE 200809L -#endif - -#include "fpk.h" -#include -#include -#include -#include -#include -#include -#include - -#ifdef _WIN32 -#include -#include -#include -#define PROGRAM_NAME "funpack.exe" -#else -#include -#define MKDIR(path) mkdir(path, 0755) -#define PROGRAM_NAME "funpack" -#endif - -#ifdef _WIN32 -static wchar_t *utf8_to_wide(const char *s) -{ - int wlen = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0); - wchar_t *w; - - if (wlen <= 0) - return NULL; - w = malloc((size_t)wlen * sizeof(wchar_t)); - if (!w) - return NULL; - MultiByteToWideChar(CP_UTF8, 0, s, -1, w, wlen); - return w; -} - -static int win_mkdir(const char *path) -{ - wchar_t *wpath = utf8_to_wide(path); - int ok; - - if (!wpath) - return -1; - ok = CreateDirectoryW(wpath, NULL); - free(wpath); - if (ok) - return 0; - if (GetLastError() == ERROR_ALREADY_EXISTS) { - errno = EEXIST; - return -1; - } - errno = 0; - return -1; -} -#define MKDIR(path) win_mkdir(path) -#endif - -static double now_seconds(void) -{ -#ifdef _WIN32 - LARGE_INTEGER freq, count; - - QueryPerformanceFrequency(&freq); - QueryPerformanceCounter(&count); - return (double)count.QuadPart / (double)freq.QuadPart; -#else - struct timespec ts; - - clock_gettime(CLOCK_MONOTONIC, &ts); - return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; -#endif -} - -static void format_duration(double secs, char *buf, size_t bufsize) -{ - unsigned long total_ms = (unsigned long)(secs * 1000.0 + 0.5); - unsigned long ms = total_ms % 1000; - unsigned long total_s = total_ms / 1000; - unsigned long s = total_s % 60; - unsigned long total_m = total_s / 60; - unsigned long m = total_m % 60; - unsigned long total_h = total_m / 60; - unsigned long h = total_h % 24; - unsigned long d = total_h / 24; - - if (d > 0) - sprintf(buf, "%lud %luh %lum %lus", d, h, m, s); - else if (h > 0) - sprintf(buf, "%luh %lum %lus", h, m, s); - else if (m > 0) - sprintf(buf, "%lum %lu.%03lus", m, s, ms); - else - sprintf(buf, "%lu.%03lus", s, ms); - buf[bufsize - 1] = '\0'; -} - -#define FUNPACK_LIST (1u << 0) -#define FUNPACK_PROGRESS (1u << 1) -#define FUNPACK_QUIET (1u << 2) -#define FUNPACK_VERBOSE (1u << 3) - -static unsigned char flags; -static volatile sig_atomic_t interrupted; - -static void handle_sigint(int sig) -{ - (void)sig; - interrupted = 1; -} - -static struct option long_opts[] = { - {"dir", required_argument, NULL, 'd'}, - {"list", no_argument, NULL, 'l'}, - {"progress", no_argument, NULL, 'p'}, - {"quiet", no_argument, NULL, 'q'}, - {"verbose", no_argument, NULL, 'v'}, - {"help", no_argument, NULL, 'h'}, - {NULL, 0, NULL, 0} -}; - -static char *dup_str(const char *s) -{ - size_t len = strlen(s) + 1; - char *out = malloc(len); - - memcpy(out, s, len); - return out; -} - -static char *join_path(const char *dir, const char *name) -{ - size_t dir_len = strlen(dir); - size_t name_len = strlen(name); - char *out = malloc(dir_len + 1 + name_len + 1); - - memcpy(out, dir, dir_len); - out[dir_len] = '/'; - memcpy(out + dir_len + 1, name, name_len); - out[dir_len + 1 + name_len] = '\0'; - return out; -} - -#ifdef _WIN32 -#define IS_PATH_SEP(c) ((c) == '/' || (c) == '\\') -#else -#define IS_PATH_SEP(c) ((c) == '/') -#endif - -static char *basename_dup(const char *path) -{ - size_t end = strlen(path); - size_t start; - char *out; - - while (end > 0 && IS_PATH_SEP(path[end - 1])) - end--; - - start = end; - while (start > 0 && !IS_PATH_SEP(path[start - 1])) - start--; - - out = malloc(end - start + 1); - memcpy(out, path + start, end - start); - out[end - start] = '\0'; - return out; -} - -static char *derive_output_dir(const char *input) -{ - char *base = basename_dup(input); - size_t len = strlen(base); - - if (len > 4 && strcmp(base + len - 4, ".fpk") == 0) - base[len - 4] = '\0'; - - return base; -} - -static int mkdir_p(const char *path) -{ - char *copy = dup_str(path); - char *p; - - for (p = copy + 1; *p; p++) { - if (*p != '/') - continue; - - *p = '\0'; - if (MKDIR(copy) != 0 && errno != EEXIST) { - fprintf(stderr, "funpack: cannot create directory: %s\n", copy); - free(copy); - return 0; - } - *p = '/'; - } - - if (MKDIR(copy) != 0 && errno != EEXIST) { - fprintf(stderr, "funpack: cannot create directory: %s\n", copy); - free(copy); - return 0; - } - - free(copy); - return 1; -} - -static int extract_entry(const char *out_dir, const fpk_entry_t *entry, - const void *data, size_t size) -{ - char *full_path = join_path(out_dir, entry->path); - FILE *fp; -#ifdef _WIN32 - wchar_t *wpath; -#endif - - if (strchr(entry->path, '/') != NULL) { - char *dir_part = dup_str(full_path); - char *slash = strrchr(dir_part, '/'); - - *slash = '\0'; - if (!mkdir_p(dir_part)) { - free(dir_part); - free(full_path); - return 0; - } - free(dir_part); - } - -#ifdef _WIN32 - wpath = utf8_to_wide(full_path); - fp = wpath ? _wfopen(wpath, L"wb") : NULL; - free(wpath); -#else - fp = fopen(full_path, "wb"); -#endif - if (!fp) { - fprintf(stderr, "funpack: cannot create: %s\n", full_path); - free(full_path); - return 0; - } - - if (fwrite(data, 1, size, fp) != size) { - fprintf(stderr, "funpack: write failed: %s\n", full_path); - fclose(fp); - free(full_path); - return 0; - } - - fclose(fp); - free(full_path); - return 1; -} - -static void usage(void) -{ - fprintf(stderr, "usage: %s [-d output_dir] [-l] [-p] [-q] [-v] input.fpk\n", PROGRAM_NAME); - fprintf(stderr, "try '%s --help' for more information\n", PROGRAM_NAME); - exit(1); -} - -static void help(void) -{ - printf("usage: %s [options] input.fpk\n", PROGRAM_NAME); - printf("\n"); - printf("Extract files from an fpk archive.\n"); - printf("\n"); - printf("options:\n"); - printf(" -d, --dir=DIR extract into DIR instead of the archive's derived name\n"); - printf(" -l, --list list archive entries and exit, without extracting\n"); - printf(" -p, --progress print extraction progress\n"); - printf(" -q, --quiet suppress the final summary line\n"); - printf(" -v, --verbose print each extracted entry\n"); - printf(" -h, --help show this help and exit\n"); - exit(0); -} - -int main(int argc, char **argv) -{ - const char *dir_arg = NULL; - const char *input; - char *out_dir; - struct fpk *pak; - uint64_t count; - uint64_t i; - int c; - double start_time; - - while ((c = getopt_long(argc, argv, "d:lpqvh", long_opts, NULL)) != -1) { - switch (c) { - case 'd': - dir_arg = optarg; - break; - case 'l': - flags |= FUNPACK_LIST; - break; - case 'p': - flags |= FUNPACK_PROGRESS; - break; - case 'q': - flags |= FUNPACK_QUIET; - break; - case 'v': - flags |= FUNPACK_VERBOSE; - break; - case 'h': - help(); - break; - default: - usage(); - } - } - - if (optind != argc - 1) - usage(); - - input = argv[optind]; - - start_time = now_seconds(); - - pak = fpk_open(input); - if (!pak) { - fprintf(stderr, "funpack: cannot open: %s\n", input); - return 1; - } - - count = fpk_entry_count(pak); - - if (flags & FUNPACK_LIST) { - for (i = 0; i < count; i++) { - const fpk_entry_t *entry = fpk_entry_at(pak, i); - - printf("%s\t%lu bytes\n", entry->path, - (unsigned long)entry->data_size); - } - - fpk_close(pak); - return 0; - } - - out_dir = dir_arg ? dup_str(dir_arg) : derive_output_dir(input); - - if (!mkdir_p(out_dir)) { - fpk_close(pak); - free(out_dir); - return 1; - } - - signal(SIGINT, handle_sigint); - - for (i = 0; i < count; i++) { - const fpk_entry_t *entry = fpk_entry_at(pak, i); - size_t size; - void *data; - - if (interrupted) - break; - - data = fpk_read(pak, entry, &size); - - if (!data) { - fprintf(stderr, "funpack: cannot read: %s\n", entry->path); - fpk_close(pak); - free(out_dir); - return 1; - } - - if (!extract_entry(out_dir, entry, data, size)) { - free(data); - fpk_close(pak); - free(out_dir); - return 1; - } - - if (flags & FUNPACK_VERBOSE) - printf(" %s (%lu bytes)\n", entry->path, (unsigned long)size); - - if (flags & FUNPACK_PROGRESS) { - printf("\runpacking %lu/%lu (%lu%%)", - (unsigned long)(i + 1), (unsigned long)count, - (unsigned long)(((i + 1) * 100) / count)); - fflush(stdout); - } - - free(data); - } - - fpk_close(pak); - - if ((flags & FUNPACK_PROGRESS) && count > 0) - printf("\n"); - - if (interrupted) { - fprintf(stderr, "funpack: interrupted, %lu of %lu entries extracted\n", - (unsigned long)i, (unsigned long)count); - free(out_dir); - return 130; - } - - if (!(flags & FUNPACK_QUIET)) { - char dur[64]; - - format_duration(now_seconds() - start_time, dur, sizeof(dur)); - printf("unpacked: %s -> %s (%lu entries) in %s\n", input, out_dir, - (unsigned long)count, dur); - } - - free(out_dir); - return 0; -}