Compare commits

..
22 Commits
Author SHA1 Message Date
anth64 9569e7c37a tools: rename fpack/funpack into fpk create/extract/list 2026-09-30 23:07:46 +02:00
anth64 430e4f3e68 docs: update changelog for 0.1.0 and 0.2.0 2026-09-16 22:38:12 +02:00
anth64 ffc283cd7e chore: bump version to 0.2.0 2026-09-16 22:36:20 +02:00
anth64 cce89bb6e4 test: cover fpk_writer_add_dir round trip 2026-09-16 22:11:52 +02:00
anth64 ba77b1d938 refactor: use fpk_writer_add_dir in fpack 2026-09-16 22:10:49 +02:00
anth64 a97b71fea9 feat: add fpk_writer_add_dir for library-level directory packing 2026-09-16 22:09:03 +02:00
anth64 cea82dfe50 feat: handle Ctrl+C gracefully, cleaning up partial output 2026-09-15 14:06:23 +02:00
anth64 e72df334ef fix: use wide-char Windows APIs for non-ASCII filenames 2026-09-15 14:01:49 +02:00
anth64 fd34aa1956 feat: format elapsed time as human-readable d/h/m/s duration 2026-09-15 12:58:45 +02:00
anth64 1286fac03f feat: print elapsed time on fpack/funpack completion 2026-09-15 12:57:04 +02:00
anth64 a327a7f85c perf: skip redundant mkdir_p in funpack for entries without subdirectories 2026-09-15 12:56:27 +02:00
anth64 d9997184fd feat: add -h/--help with full option descriptions to fpack/funpack 2026-09-15 12:44:58 +02:00
anth64 01aebb3f31 fix: recognize backslash separators in fpack basename/relpath (Windows only) 2026-09-15 12:43:58 +02:00
anth64 bfba351ac2 build: release target also builds tools 2026-09-15 12:13:08 +02:00
anth64 5f4c38a0e4 fix: use 64-bit seek/tell in fpack, check ftell errors 2026-09-15 11:00:24 +02:00
anth64 4b5f67c0ba fix: avoid failing open on empty archives (malloc(0) returns NULL) 2026-09-15 10:52:03 +02:00
anth64 c08d3b097a fix: use 64-bit seek, avoid 32-bit long truncation on large archives 2026-09-15 10:51:55 +02:00
anth64 ebd3b0522f docs: update README usage to match actual fpack/funpack CLI behavior 2026-09-15 10:33:43 +02:00
anth64 dd669fb9b0 fix: print bare program name in usage instead of argv[0] path 2026-09-15 10:29:45 +02:00
anth64 a0a08cb73a feat: add fpack/funpack CLI tools 2026-09-15 07:44:31 +02:00
anth64 11675f8cd4 feat: implement archive reader/writer with round-trip test 2026-09-14 23:26:02 +02:00
anth64 8287541447 feat: add public API header and format spec 2026-09-14 23:20:32 +02:00
14 changed files with 1696 additions and 9 deletions
+29
View File
@@ -6,3 +6,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [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
- `fpk_writer_add_dir()` for library-level recursive directory packing.
## [0.1.0] - 2026-09-15
### Added
- `FPK1` archive format: flat header/data-blobs/string-table/directory layout,
fixed little-endian on disk.
- Reader API: `fpk_open`/`fpk_close`/`fpk_find`/`fpk_read`/`fpk_entry_count`/
`fpk_entry_at`.
- Writer API: `fpk_writer_open`/`fpk_writer_add`/`fpk_writer_finalize`/
`fpk_writer_close`.
- `fpack`/`funpack` CLI tools with full flag sets (`-o`/`-p`/`-q`/`-v`/`-h` and
`-d`/`-l`/`-p`/`-q`/`-v`/`-h` respectively).
- Graceful Ctrl+C handling in `fpack`, cleaning up partial output.
- Elapsed-time reporting on `fpack`/`funpack` completion.
- Wide-char Windows filename support in `funpack`.
- 64-bit seek/tell throughout, avoiding 32-bit truncation on large archives.
+19 -4
View File
@@ -14,15 +14,30 @@ build.bat debug release tools # Windows
## Usage ## Usage
```bash ```bash
fpack output.fpk assets/ fpk create [-o output.fpk] [-p] [-q] [-v] input_dir
funpack output.fpk [unpacked/] # dir defaults to "output" if omitted fpk extract [-d output_dir] [-p] [-q] [-v] input.fpk
fpk list input.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`).
`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.
```c ```c
#include <fpk/fpk.h> #include <fpk/fpk.h>
fpk_t *pak = fpk_open("assets.fpk"); struct fpk *pak = fpk_open("assets.fpk");
fpk_find(pak, "some/file.bin", &data, &size); fpk_entry_t *entry = fpk_find(pak, "some/file.bin");
void *data = fpk_read(pak, entry, &size);
fpk_close(pak); fpk_close(pak);
``` ```
+1 -1
View File
@@ -18,7 +18,7 @@ TOOL_DEBUG_BINS = ${TOOL_SRCS:S|^tools/|${.CURDIR}/${BIN_DIR}/debug/tools/|:S/
TOOL_RELEASE_BINS = ${TOOL_SRCS:S|^tools/|${.CURDIR}/${BIN_DIR}/release/tools/|:S/.c$//} TOOL_RELEASE_BINS = ${TOOL_SRCS:S|^tools/|${.CURDIR}/${BIN_DIR}/release/tools/|:S/.c$//}
debug: ${.CURDIR}/${BIN_DIR}/debug/${STATIC_LIB} debug: ${.CURDIR}/${BIN_DIR}/debug/${STATIC_LIB}
release: ${.CURDIR}/${BIN_DIR}/release/${STATIC_LIB} release: ${.CURDIR}/${BIN_DIR}/release/${STATIC_LIB} ${TOOL_RELEASE_BINS}
tools: ${TOOL_DEBUG_BINS} tools: ${TOOL_DEBUG_BINS}
+4 -3
View File
@@ -6,8 +6,9 @@ BIN_DIR = bin
LIB_NAME = fpk LIB_NAME = fpk
STATIC_LIB = lib$(LIB_NAME).a STATIC_LIB = lib$(LIB_NAME).a
SRCS = src/fpk.c SRCS = \
src/fpk.c \
src/fpk_dir.c
TOOL_SRCS = \ TOOL_SRCS = \
tools/fpack.c \ tools/fpk.c
tools/funpack.c
+52
View File
@@ -0,0 +1,52 @@
# FPK1 format
```
[ header ][ data blobs ][ string table ][ directory entries ]
```
## Header (28 bytes)
| Field | Type | Size | Offset |
|-----------------|-----------|------|--------|
| `magic` | char[4] | 4 | 0 |
| `entry_count` | uint64_t | 8 | 4 |
| `dir_offset` | uint64_t | 8 | 12 |
| `strtab_offset` | uint64_t | 8 | 20 |
`magic` = `"FPK1"`.
`FPK_HEADER_SIZE` (28): field-by-field, not `sizeof()`.
## Data blobs
Raw bytes, back-to-back, no padding, writer add order.
## String table
Paths, `/`-separated, NUL-terminated, back-to-back, entry order.
`strtab_offset` points here.
## Directory (25 bytes per entry)
| Field | Type | Size | Offset |
|---------------|----------|------|--------|
| `path_offset` | uint64_t | 8 | 0 |
| `data_offset` | uint64_t | 8 | 8 |
| `data_size` | uint64_t | 8 | 16 |
| `method` | uint8_t | 1 | 24 |
`path_offset` relative to `strtab_offset`. `data_offset` absolute.
`FPK_DIR_ENTRY_SIZE` (25): field-by-field, not `sizeof()`.
`dir_offset` points here, last region. File size = `dir_offset +
entry_count * FPK_DIR_ENTRY_SIZE`.
## Compression
`method`: reserved, v1 = `FPK_METHOD_STORE` (0) only.
## Endianness
Little-endian on disk, always. Big-endian host swaps on read/write.
+1 -1
View File
@@ -30,7 +30,7 @@ TOOL_RELEASE_BINS := $(TOOL_SRCS:tools/%.c=$(BIN_DIR)/release/tools/%$(TOOL_EXT)
all: debug tools all: debug tools
debug: $(BIN_DIR)/debug/$(STATIC_LIB) debug: $(BIN_DIR)/debug/$(STATIC_LIB)
release: $(BIN_DIR)/release/$(STATIC_LIB) release: $(BIN_DIR)/release/$(STATIC_LIB) $(TOOL_RELEASE_BINS)
tools: $(TOOL_DEBUG_BINS) tools: $(TOOL_DEBUG_BINS)
+67
View File
@@ -0,0 +1,67 @@
#ifndef FPK_H
#define FPK_H
#include <stddef.h>
#include <stdint.h>
#define FPK_MAGIC "FPK1"
#define FPK_MAGIC_SIZE 4
#define FPK_VERSION 1
#define FPK_HEADER_SIZE 28
/*
* magic 4
* entry_count 8
* dir_offset 8
* strtab_offset 8
* --
* 28
*/
#define FPK_DIR_ENTRY_SIZE 25
/*
* path_offset 8
* data_offset 8
* data_size 8
* method 1
* --
* 25
*/
#define FPK_METHOD_STORE 0
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
const char *path;
uint64_t data_offset;
uint64_t data_size;
uint8_t method;
} fpk_entry_t;
struct fpk;
struct fpk_writer;
struct fpk *fpk_open(const char *path);
void fpk_close(struct fpk *fpk);
fpk_entry_t *fpk_find(struct fpk *fpk, const char *path);
void *fpk_read(struct fpk *fpk, const fpk_entry_t *entry, size_t *out_size);
uint64_t fpk_entry_count(const struct fpk *fpk);
const fpk_entry_t *fpk_entry_at(const struct fpk *fpk, uint64_t index);
struct fpk_writer *fpk_writer_open(const char *path);
int fpk_writer_add(struct fpk_writer *w, const char *path, const void *data,
size_t size);
int fpk_writer_finalize(struct fpk_writer *w);
void fpk_writer_close(struct fpk_writer *w);
typedef int (*fpk_add_dir_cb)(const char *path, uint64_t size, void *ctx);
int fpk_writer_add_dir(struct fpk_writer *w, const char *dir_path,
fpk_add_dir_cb on_add, void *ctx);
#ifdef __cplusplus
}
#endif
#endif /* FPK_H */
+16
View File
@@ -0,0 +1,16 @@
#ifndef FPK_VERSION_H
#define FPK_VERSION_H
#define FPK_VERSION_MAJOR 0
#define FPK_VERSION_MINOR 2
#define FPK_VERSION_PATCH 0
#define FPK_STRINGIFY_HELPER(x) #x
#define FPK_STRINGIFY(x) FPK_STRINGIFY_HELPER(x)
#define FPK_VERSION_STRING \
FPK_STRINGIFY(FPK_VERSION_MAJOR) \
"." FPK_STRINGIFY(FPK_VERSION_MINOR) "." FPK_STRINGIFY( \
FPK_VERSION_PATCH)
#endif /* FPK_VERSION_H */
+371
View File
@@ -0,0 +1,371 @@
#if !defined(_WIN32)
#define _FILE_OFFSET_BITS 64
#define _POSIX_C_SOURCE 200809L
#endif
#include "fpk.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(_WIN32)
#define fpk_fseek64(f, off) _fseeki64((f), (off), SEEK_SET)
#else
#include <sys/types.h>
#define fpk_fseek64(f, off) fseeko((f), (off_t)(off), SEEK_SET)
#endif
#define FPK_HEADER_FIELD_ENTRY_COUNT 4
#define FPK_HEADER_FIELD_DIR_OFFSET 12
#define FPK_HEADER_FIELD_STRTAB_OFFSET 20
#define FPK_DIR_ENTRY_FIELD_PATH_OFFSET 0
#define FPK_DIR_ENTRY_FIELD_DATA_OFFSET 8
#define FPK_DIR_ENTRY_FIELD_DATA_SIZE 16
#define FPK_DIR_ENTRY_FIELD_METHOD 24
struct fpk {
FILE *file;
uint64_t entry_count;
fpk_entry_t *entries;
char *strtab;
};
struct fpk_writer_entry {
char *path;
uint64_t data_offset;
uint64_t data_size;
uint8_t method;
};
struct fpk_writer {
FILE *file;
struct fpk_writer_entry *entries;
uint64_t count;
uint64_t capacity;
uint64_t cursor;
};
static uint32_t get_u32le(const unsigned char *p)
{
return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 |
(uint32_t)p[3] << 24;
}
static uint64_t get_u64le(const unsigned char *p)
{
return (uint64_t)get_u32le(p) | (uint64_t)get_u32le(p + 4) << 32;
}
static void put_u32le(unsigned char *p, uint32_t v)
{
p[0] = (unsigned char)(v);
p[1] = (unsigned char)(v >> 8);
p[2] = (unsigned char)(v >> 16);
p[3] = (unsigned char)(v >> 24);
}
static void put_u64le(unsigned char *p, uint64_t v)
{
put_u32le(p, (uint32_t)v);
put_u32le(p + 4, (uint32_t)(v >> 32));
}
static char *dup_path(const char *path)
{
size_t len = strlen(path) + 1;
char *copy = malloc(len);
if (copy != NULL)
memcpy(copy, path, len);
return copy;
}
struct fpk *fpk_open(const char *path)
{
struct fpk *fpk;
unsigned char header[FPK_HEADER_SIZE];
uint64_t dir_offset, strtab_offset, strtab_size;
uint64_t i;
fpk = malloc(sizeof(*fpk));
if (fpk == NULL)
goto out;
fpk->file = NULL;
fpk->entries = NULL;
fpk->strtab = NULL;
fpk->file = fopen(path, "rb");
if (fpk->file == NULL)
goto fail;
if (fread(header, 1, FPK_HEADER_SIZE, fpk->file) != FPK_HEADER_SIZE)
goto fail;
if (memcmp(header, FPK_MAGIC, FPK_MAGIC_SIZE) != 0)
goto fail;
fpk->entry_count = get_u64le(header + FPK_HEADER_FIELD_ENTRY_COUNT);
dir_offset = get_u64le(header + FPK_HEADER_FIELD_DIR_OFFSET);
strtab_offset = get_u64le(header + FPK_HEADER_FIELD_STRTAB_OFFSET);
if (dir_offset < strtab_offset)
goto fail;
strtab_size = dir_offset - strtab_offset;
if (strtab_size > 0) {
fpk->strtab = malloc((size_t)strtab_size);
if (fpk->strtab == NULL)
goto fail;
if (fpk_fseek64(fpk->file, strtab_offset) != 0)
goto fail;
if (fread(fpk->strtab, 1, (size_t)strtab_size, fpk->file) !=
strtab_size)
goto fail;
}
if (fpk->entry_count > 0) {
fpk->entries = malloc(
(size_t)fpk->entry_count * sizeof(fpk_entry_t));
if (fpk->entries == NULL)
goto fail;
}
if (fpk_fseek64(fpk->file, dir_offset) != 0)
goto fail;
for (i = 0; i < fpk->entry_count; i++) {
unsigned char rec[FPK_DIR_ENTRY_SIZE];
uint64_t path_offset;
if (fread(rec, 1, FPK_DIR_ENTRY_SIZE, fpk->file) !=
FPK_DIR_ENTRY_SIZE)
goto fail;
path_offset = get_u64le(rec + FPK_DIR_ENTRY_FIELD_PATH_OFFSET);
if (path_offset >= strtab_size)
goto fail;
fpk->entries[i].path = fpk->strtab + path_offset;
fpk->entries[i].data_offset =
get_u64le(rec + FPK_DIR_ENTRY_FIELD_DATA_OFFSET);
fpk->entries[i].data_size =
get_u64le(rec + FPK_DIR_ENTRY_FIELD_DATA_SIZE);
fpk->entries[i].method = rec[FPK_DIR_ENTRY_FIELD_METHOD];
}
goto out;
fail:
fpk_close(fpk);
fpk = NULL;
out:
return fpk;
}
void fpk_close(struct fpk *fpk)
{
if (fpk == NULL)
return;
if (fpk->file != NULL)
fclose(fpk->file);
free(fpk->entries);
free(fpk->strtab);
free(fpk);
}
fpk_entry_t *fpk_find(struct fpk *fpk, const char *path)
{
uint64_t i;
for (i = 0; i < fpk->entry_count; i++) {
if (strcmp(fpk->entries[i].path, path) == 0)
return &fpk->entries[i];
}
return NULL;
}
void *fpk_read(struct fpk *fpk, const fpk_entry_t *entry, size_t *out_size)
{
void *buf;
buf = malloc((size_t)entry->data_size);
if (buf == NULL)
goto out;
if (fpk_fseek64(fpk->file, entry->data_offset) != 0 ||
fread(buf, 1, (size_t)entry->data_size, fpk->file) !=
entry->data_size) {
free(buf);
buf = NULL;
goto out;
}
*out_size = (size_t)entry->data_size;
out:
return buf;
}
uint64_t fpk_entry_count(const struct fpk *fpk)
{
return fpk->entry_count;
}
const fpk_entry_t *fpk_entry_at(const struct fpk *fpk, uint64_t index)
{
if (index >= fpk->entry_count)
return NULL;
return &fpk->entries[index];
}
#define FPK_WRITER_INITIAL_CAPACITY 16
struct fpk_writer *fpk_writer_open(const char *path)
{
struct fpk_writer *w;
unsigned char placeholder[FPK_HEADER_SIZE];
w = malloc(sizeof(*w));
if (w == NULL)
goto out;
w->entries = NULL;
w->count = 0;
w->capacity = 0;
w->file = fopen(path, "wb");
if (w->file == NULL)
goto fail_free;
memset(placeholder, 0, sizeof(placeholder));
if (fwrite(placeholder, 1, FPK_HEADER_SIZE, w->file) !=
FPK_HEADER_SIZE)
goto fail_close;
w->cursor = FPK_HEADER_SIZE;
goto out;
fail_close:
fclose(w->file);
fail_free:
free(w);
w = NULL;
out:
return w;
}
int fpk_writer_add(struct fpk_writer *w, const char *path, const void *data,
size_t size)
{
struct fpk_writer_entry *entry;
char *path_copy = NULL;
int ret = 0;
if (w->count == w->capacity) {
uint64_t new_capacity = w->capacity == 0
? FPK_WRITER_INITIAL_CAPACITY
: w->capacity * 2;
struct fpk_writer_entry *new_entries = realloc(
w->entries, (size_t)new_capacity * sizeof(*new_entries));
if (new_entries == NULL) {
ret = -1;
goto out;
}
w->entries = new_entries;
w->capacity = new_capacity;
}
path_copy = dup_path(path);
if (path_copy == NULL) {
ret = -1;
goto out;
}
if (size > 0 && fwrite(data, 1, size, w->file) != size) {
ret = -1;
goto out;
}
entry = &w->entries[w->count];
entry->path = path_copy;
entry->data_offset = w->cursor;
entry->data_size = size;
entry->method = FPK_METHOD_STORE;
w->cursor += size;
w->count++;
out:
if (ret != 0)
free(path_copy);
return ret;
}
int fpk_writer_finalize(struct fpk_writer *w)
{
uint64_t strtab_offset, dir_offset, path_offset;
unsigned char header[FPK_HEADER_SIZE];
unsigned char rec[FPK_DIR_ENTRY_SIZE];
uint64_t i;
int ret = 0;
strtab_offset = w->cursor;
if (fpk_fseek64(w->file, strtab_offset) != 0) {
ret = -1;
goto out;
}
path_offset = 0;
for (i = 0; i < w->count && ret == 0; i++) {
size_t len = strlen(w->entries[i].path) + 1;
if (fwrite(w->entries[i].path, 1, len, w->file) != len)
ret = -1;
else
path_offset += len;
}
if (ret != 0)
goto out;
dir_offset = strtab_offset + path_offset;
path_offset = 0;
for (i = 0; i < w->count && ret == 0; i++) {
put_u64le(rec + FPK_DIR_ENTRY_FIELD_PATH_OFFSET, path_offset);
put_u64le(rec + FPK_DIR_ENTRY_FIELD_DATA_OFFSET,
w->entries[i].data_offset);
put_u64le(rec + FPK_DIR_ENTRY_FIELD_DATA_SIZE,
w->entries[i].data_size);
rec[FPK_DIR_ENTRY_FIELD_METHOD] = w->entries[i].method;
if (fwrite(rec, 1, FPK_DIR_ENTRY_SIZE, w->file) !=
FPK_DIR_ENTRY_SIZE)
ret = -1;
else
path_offset += strlen(w->entries[i].path) + 1;
}
if (ret != 0)
goto out;
memcpy(header, FPK_MAGIC, FPK_MAGIC_SIZE);
put_u64le(header + FPK_HEADER_FIELD_ENTRY_COUNT, w->count);
put_u64le(header + FPK_HEADER_FIELD_DIR_OFFSET, dir_offset);
put_u64le(header + FPK_HEADER_FIELD_STRTAB_OFFSET, strtab_offset);
if (fpk_fseek64(w->file, 0) != 0 ||
fwrite(header, 1, FPK_HEADER_SIZE, w->file) != FPK_HEADER_SIZE ||
fflush(w->file) != 0)
ret = -1;
out:
return ret;
}
void fpk_writer_close(struct fpk_writer *w)
{
uint64_t i;
if (w == NULL)
return;
if (w->file != NULL)
fclose(w->file);
for (i = 0; i < w->count; i++)
free(w->entries[i].path);
free(w->entries);
free(w);
}
+289
View File
@@ -0,0 +1,289 @@
#if !defined(_WIN32)
#define _FILE_OFFSET_BITS 64
#define _POSIX_C_SOURCE 200809L
#endif
#include "fpk.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#include <wchar.h>
#define fpk_dir_fseek64(f, off, whence) _fseeki64((f), (off), (whence))
#define fpk_dir_ftell64(f) _ftelli64((f))
#else
#include <dirent.h>
#include <sys/stat.h>
#define fpk_dir_fseek64(f, off, whence) fseeko((f), (off_t)(off), (whence))
#define fpk_dir_ftell64(f) ftello((f))
#endif
#ifdef _WIN32
#define IS_PATH_SEP(c) ((c) == '/' || (c) == '\\')
#else
#define IS_PATH_SEP(c) ((c) == '/')
#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 char *wide_to_utf8(const wchar_t *w)
{
int len = WideCharToMultiByte(CP_UTF8, 0, w, -1, NULL, 0, NULL, NULL);
char *s;
if (len <= 0)
return NULL;
s = malloc((size_t)len);
if (!s)
return NULL;
WideCharToMultiByte(CP_UTF8, 0, w, -1, s, len, NULL, NULL);
return s;
}
#endif
static char *dup_str(const char *s)
{
size_t len = strlen(s) + 1;
char *out = malloc(len);
if (out != NULL)
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);
if (out == NULL)
return NULL;
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;
}
static void normalize_path(char *s)
{
for (; *s; s++) {
if (*s == '\\')
*s = '/';
}
}
static void *read_file(const char *path, size_t *out_size)
{
FILE *fp;
int64_t len;
void *buf;
#ifdef _WIN32
wchar_t *wpath = utf8_to_wide(path);
if (!wpath)
return NULL;
fp = _wfopen(wpath, L"rb");
free(wpath);
#else
fp = fopen(path, "rb");
#endif
if (!fp)
return NULL;
if (fpk_dir_fseek64(fp, 0, SEEK_END) != 0) {
fclose(fp);
return NULL;
}
len = fpk_dir_ftell64(fp);
if (len < 0 || fpk_dir_fseek64(fp, 0, SEEK_SET) != 0) {
fclose(fp);
return NULL;
}
buf = malloc((size_t)len);
if (!buf) {
fclose(fp);
return NULL;
}
if (fread(buf, 1, (size_t)len, fp) != (size_t)len) {
free(buf);
fclose(fp);
return NULL;
}
fclose(fp);
*out_size = (size_t)len;
return buf;
}
static int add_file(struct fpk_writer *w, const char *path, size_t base_len,
fpk_add_dir_cb on_add, void *ctx)
{
const char *rel_path = path + base_len;
void *data;
size_t size;
char *norm;
int ret;
while (*rel_path == '/')
rel_path++;
norm = dup_str(rel_path);
if (norm == NULL)
return 0;
normalize_path(norm);
data = read_file(path, &size);
if (data == NULL) {
free(norm);
return 0;
}
if (fpk_writer_add(w, norm, data, size) != 0) {
free(data);
free(norm);
return 0;
}
ret = on_add == NULL || on_add(norm, (uint64_t)size, ctx) != 0;
free(data);
free(norm);
return ret;
}
#ifdef _WIN32
static int add_dir(struct fpk_writer *w, const char *dir, size_t base_len,
fpk_add_dir_cb on_add, void *ctx)
{
char *pattern = join_path(dir, "*");
wchar_t *wpattern;
WIN32_FIND_DATAW fd;
HANDLE h;
if (pattern == NULL)
return 0;
wpattern = utf8_to_wide(pattern);
free(pattern);
if (wpattern == NULL)
return 0;
h = FindFirstFileW(wpattern, &fd);
free(wpattern);
if (h == INVALID_HANDLE_VALUE)
return 0;
do {
char *name = wide_to_utf8(fd.cFileName);
char *path;
if (name == NULL || name[0] == '.') {
free(name);
continue;
}
path = join_path(dir, name);
free(name);
if (path == NULL) {
FindClose(h);
return 0;
}
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
if (!add_dir(w, path, base_len, on_add, ctx)) {
free(path);
FindClose(h);
return 0;
}
} else if (!add_file(w, path, base_len, on_add, ctx)) {
free(path);
FindClose(h);
return 0;
}
free(path);
} while (FindNextFileW(h, &fd));
FindClose(h);
return 1;
}
#else
static int add_dir(struct fpk_writer *w, const char *dir, size_t base_len,
fpk_add_dir_cb on_add, void *ctx)
{
DIR *d;
struct dirent *ent;
d = opendir(dir);
if (d == NULL)
return 0;
while ((ent = readdir(d)) != NULL) {
char *path;
struct stat st;
if (ent->d_name[0] == '.')
continue;
path = join_path(dir, ent->d_name);
if (path == NULL) {
closedir(d);
return 0;
}
if (stat(path, &st) != 0) {
free(path);
continue;
}
if (S_ISDIR(st.st_mode)) {
if (!add_dir(w, path, base_len, on_add, ctx)) {
free(path);
closedir(d);
return 0;
}
} else if (S_ISREG(st.st_mode)) {
if (!add_file(w, path, base_len, on_add, ctx)) {
free(path);
closedir(d);
return 0;
}
}
free(path);
}
closedir(d);
return 1;
}
#endif
int fpk_writer_add_dir(struct fpk_writer *w, const char *dir_path,
fpk_add_dir_cb on_add, void *ctx)
{
size_t base_len = strlen(dir_path);
while (base_len > 0 && IS_PATH_SEP(dir_path[base_len - 1]))
base_len--;
return add_dir(w, dir_path, base_len, on_add, ctx) ? 0 : -1;
}
+16
View File
@@ -0,0 +1,16 @@
CC ?= cc
CFLAGS = -Wall -Wpedantic -I../include -std=c89
LDFLAGS = -L../bin/debug -lfpk
.PHONY: all test clean
all: test
test_program: test.c
${CC} ${CFLAGS} -o ${.TARGET} test.c ${LDFLAGS}
test: test_program
@./test_program
clean:
rm -f test_program
+28
View File
@@ -0,0 +1,28 @@
CC := cc
CFLAGS = -Wall -Wpedantic -I../include -std=c89
LDFLAGS = -L../bin/debug -lfpk
ifeq ($(OS),Windows_NT)
SHELL := cmd.exe
.SHELLFLAGS := /c
EXE_EXT = .exe
else
EXE_EXT =
endif
.PHONY: all test clean
all: test
test_program$(EXE_EXT): test.c
$(CC) $(CFLAGS) -o $@ test.c $(LDFLAGS)
test: test_program$(EXE_EXT)
@./test_program$(EXE_EXT)
clean:
ifeq ($(OS),Windows_NT)
@del /Q test_program.exe 2>nul || true
else
@rm -f test_program
endif
+129
View File
@@ -0,0 +1,129 @@
#include <fpk.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <direct.h>
#define MKDIR(path) _mkdir(path)
#define RMDIR(path) _rmdir(path)
#else
#include <sys/stat.h>
#include <unistd.h>
#define MKDIR(path) mkdir(path, 0755)
#define RMDIR(path) rmdir(path)
#endif
#define CHECK(cond, msg) \
do { \
if (!(cond)) { \
fprintf(stderr, "FAIL: %s\n", msg); \
return EXIT_FAILURE; \
} \
} while (0)
static const char *PAK_PATH = "roundtrip.fpk";
static void write_file(const char *path, const char *data)
{
FILE *fp = fopen(path, "wb");
fwrite(data, 1, strlen(data), fp);
fclose(fp);
}
int main(void)
{
struct fpk_writer *w;
struct fpk *r;
fpk_entry_t *entry;
void *data;
size_t out_size;
static const char hello[] = "hello, fpk";
static const char world[] = "world";
w = fpk_writer_open(PAK_PATH);
CHECK(w != NULL, "fpk_writer_open failed");
CHECK(fpk_writer_add(w, "a/hello.txt", hello, sizeof(hello)) == 0,
"fpk_writer_add(hello) failed");
CHECK(fpk_writer_add(w, "b/world.txt", world, sizeof(world)) == 0,
"fpk_writer_add(world) failed");
CHECK(fpk_writer_add(w, "c/empty.bin", NULL, 0) == 0,
"fpk_writer_add(empty) failed");
CHECK(fpk_writer_finalize(w) == 0, "fpk_writer_finalize failed");
fpk_writer_close(w);
r = fpk_open(PAK_PATH);
CHECK(r != NULL, "fpk_open failed");
CHECK(fpk_entry_count(r) == 3, "unexpected entry_count");
entry = fpk_find(r, "a/hello.txt");
CHECK(entry != NULL, "fpk_find(hello) failed");
CHECK(entry->data_size == sizeof(hello), "hello size mismatch");
data = fpk_read(r, entry, &out_size);
CHECK(data != NULL, "fpk_read(hello) failed");
CHECK(out_size == sizeof(hello), "hello read size mismatch");
CHECK(memcmp(data, hello, sizeof(hello)) == 0, "hello content mismatch");
free(data);
entry = fpk_find(r, "b/world.txt");
CHECK(entry != NULL, "fpk_find(world) failed");
data = fpk_read(r, entry, &out_size);
CHECK(data != NULL, "fpk_read(world) failed");
CHECK(out_size == sizeof(world), "world read size mismatch");
CHECK(memcmp(data, world, sizeof(world)) == 0, "world content mismatch");
free(data);
entry = fpk_find(r, "c/empty.bin");
CHECK(entry != NULL, "fpk_find(empty) failed");
CHECK(entry->data_size == 0, "empty size mismatch");
CHECK(fpk_find(r, "does/not/exist") == NULL,
"fpk_find(missing) should return NULL");
fpk_close(r);
remove(PAK_PATH);
CHECK(MKDIR("adddir_src") == 0, "mkdir(adddir_src) failed");
CHECK(MKDIR("adddir_src/nested") == 0, "mkdir(adddir_src/nested) failed");
write_file("adddir_src/root.txt", "root");
write_file("adddir_src/nested/leaf.txt", "leaf");
w = fpk_writer_open("adddir.fpk");
CHECK(w != NULL, "fpk_writer_open(adddir) failed");
CHECK(fpk_writer_add_dir(w, "adddir_src", NULL, NULL) == 0,
"fpk_writer_add_dir failed");
CHECK(fpk_writer_finalize(w) == 0, "fpk_writer_finalize(adddir) failed");
fpk_writer_close(w);
r = fpk_open("adddir.fpk");
CHECK(r != NULL, "fpk_open(adddir) failed");
CHECK(fpk_entry_count(r) == 2, "adddir entry_count mismatch");
entry = fpk_find(r, "root.txt");
CHECK(entry != NULL, "fpk_find(root.txt) failed");
data = fpk_read(r, entry, &out_size);
CHECK(data != NULL, "fpk_read(root.txt) failed");
CHECK(out_size == 4 && memcmp(data, "root", 4) == 0,
"root.txt content mismatch");
free(data);
entry = fpk_find(r, "nested/leaf.txt");
CHECK(entry != NULL, "fpk_find(nested/leaf.txt) failed");
data = fpk_read(r, entry, &out_size);
CHECK(data != NULL, "fpk_read(nested/leaf.txt) failed");
CHECK(out_size == 4 && memcmp(data, "leaf", 4) == 0,
"nested/leaf.txt content mismatch");
free(data);
fpk_close(r);
remove("adddir.fpk");
remove("adddir_src/root.txt");
remove("adddir_src/nested/leaf.txt");
RMDIR("adddir_src/nested");
RMDIR("adddir_src");
printf("PASS\n");
return EXIT_SUCCESS;
}
+674
View File
@@ -0,0 +1,674 @@
#if !defined(_WIN32)
#define _POSIX_C_SOURCE 200809L
#endif
#include "fpk.h"
#include <errno.h>
#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#ifdef _WIN32
#include <direct.h>
#include <windows.h>
#include <wchar.h>
#else
#include <time.h>
#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 <create|extract|list> [options] ...\n");
fprintf(stderr, "try 'fpk --help' for more information\n");
exit(1);
}
static void top_help(void)
{
printf("usage: fpk <command> [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 <command> --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;
}