pak

pak.git
git clone git://git.lenczewski.org/pak.git
Log | Files | Refs | README | LICENSE

commit 87b0e2d92376cb3082b1c2767a3c362a8c653adb
Author: Mikołaj Lenczewski <mblenczewski@gmail.com>
Date:   Mon, 28 Apr 2025 16:15:23 +0000

Initial commit

Diffstat:
A.editorconfig | 18++++++++++++++++++
A.gitignore | 3+++
ALICENSE | 18++++++++++++++++++
AREADME | 4++++
Aalloc.h | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abuild.sh | 9+++++++++
Aclean.sh | 5+++++
Alist.h | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apak.c | 745+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apak.h | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apakfmt.h | 205+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest.c | 295+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 1529 insertions(+), 0 deletions(-)

diff --git a/.editorconfig b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +# visual studio editorconfig plugin specific settings +guidelines = 80, 120, 160 + +[*.{c,h}] +indent_style = tab +indent_size = 8 + +[*.sh] +indent_style = tab +indent_size = 8 diff --git a/.gitignore b/.gitignore @@ -0,0 +1,3 @@ +bin/ + +**/.*.swp diff --git a/LICENSE b/LICENSE @@ -0,0 +1,18 @@ +The MIT-Zero License + +Copyright (c) 2025 Mikołaj Lenczewski <mikolaj@lenczewski.org> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README b/README @@ -0,0 +1,4 @@ +pak +============================================================================== +An example archive file format. Includes code demonstrating loading from files +or from memory, and intended usage. Includes limited test code. diff --git a/alloc.h b/alloc.h @@ -0,0 +1,57 @@ +#ifndef ALLOC_H +#define ALLOC_H + +#include <assert.h> +#include <stdalign.h> +#include <stdint.h> +#include <stddef.h> + +#define IS_POW2(v) (((v) & ((v) - 1)) == 0) +#define IS_ALIGNED(v, align) (((v) & ((align) - 1)) == 0) +#define ALIGN_PREV(v, align) ((v) & ~((align) - 1)) +#define ALIGN_NEXT(v, align) ALIGN_PREV((v) + ((align) - 1), (align)) + +struct arena { + void *ptr; + uint64_t cap, len; +}; + +inline void +arena_reset(struct arena *arena) +{ + arena->len = 0; +} + +inline void * +arena_alloc(struct arena *arena, size_t size, size_t align) +{ + assert(size); + assert(align); + assert(IS_POW2(align)); + + uintptr_t aligned_ptr = ALIGN_NEXT((uintptr_t) arena->ptr + arena->len, align); + if (aligned_ptr + size > (uintptr_t) arena->ptr + arena->cap) + return NULL; + + arena->len = (aligned_ptr - (uintptr_t) arena->ptr) + size; + + return (void *) aligned_ptr; +} + +#define ARENA_ALLOC_ARRAY(arena, T, n) \ + (T *) arena_alloc((arena), sizeof(T) * (n), alignof(T)) + +#define ARENA_ALLOC_SIZED(arena, T) ARENA_ALLOC_ARRAY((arena), T, 1) + +#endif /* ALLOC_H */ + +#ifdef HEADER_IMPL + +extern inline void +arena_reset(struct arena *arena); + + +extern inline void * +arena_alloc(struct arena *arena, size_t size, size_t align); + +#endif /* HEADER_IMPL */ diff --git a/build.sh b/build.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +FLAGS="-Wall -Wextra -Wpedantic -Wno-format-pedantic ${WERROR:+-Werror} -std=c11 -O0 -g" + +set -ex + +mkdir -p bin + +clang -o bin/test test.c $FLAGS diff --git a/clean.sh b/clean.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +set -ex + +rm -rf bin diff --git a/list.h b/list.h @@ -0,0 +1,111 @@ +#ifndef LIST_H +#define LIST_H + +#include <stddef.h> +#include <stdint.h> + +#define TO_PARENT(ptr, T, member) \ + ((ptr) ? ((T *) (((uintptr_t) (ptr)) - offsetof(T, member))) : NULL) + +struct list_node { + struct list_node *prev, *next; +}; + +#define FROM_LIST_NODE(node, T, member) \ + TO_PARENT((node), T, member) + +#define LIST_NODE_ENTRY(node, it, member) \ + FROM_LIST_NODE((node), __typeof__ (*(it)), member) + +#define LIST_INIT(list) ((struct list_node) { &(list), &(list), }) + +#define LIST_HEAD(list) ((list)->next) +#define LIST_TAIL(list) ((list)->prev) +#define LIST_EMPTY(list) ((list)->prev == (list) && (list)->next == (list)) + +// iterate by list_node +#define LIST_NODE_ITER(list, it) \ + for ((it) = LIST_HEAD(list); (it) != (list); (it) = (it)->next) + +// iterare by list_node in reverse +#define LIST_NODE_RITER(list, it) \ + for ((it) = LIST_TAIL(list); (it) != (list); (it) = (it)->prev) + +// iterate by container type T +#define LIST_ENTRY_ITER(list, it, member) \ + for ((it) = LIST_NODE_ENTRY(LIST_HEAD(list), (it), member); \ + &(it)->member != (list); \ + (it) = LIST_NODE_ENTRY((it)->member.next, (it), member)) + +// iterate by container type in reverse +#define LIST_ENTRY_RITER(list, it, member) \ + for ((it) = LIST_NODE_ENTRY(LIST_TAIL(list), (it), member); \ + &(it)->member != (list); \ + (it) = LIST_NODE_ENTRY((it)->member.prev, (it), member)) + +inline void +list_node_link(struct list_node *node, struct list_node *prev, struct list_node *next) +{ + prev->next = node; + node->prev = prev; + next->prev = node; + node->next = next; +} + +inline struct list_node * +list_node_unlink(struct list_node *node) +{ + node->prev->next = node->next; + node->next->prev = node->prev; + return node; +} + +inline void +list_push_head(struct list_node *restrict list, struct list_node *restrict node) +{ + list_node_link(node, list, LIST_HEAD(list)); +} + +inline void +list_push_tail(struct list_node *restrict list, struct list_node *restrict node) +{ + list_node_link(node, LIST_TAIL(list), list); +} + +inline struct list_node * +list_pop_head(struct list_node *list) +{ + struct list_node *res = list_node_unlink(LIST_HEAD(list)); + return res; +} + +inline struct list_node * +list_pop_tail(struct list_node *list) +{ + struct list_node *res = list_node_unlink(LIST_TAIL(list)); + return res; +} + +#endif /* LIST_H */ + +#ifdef HEADER_IMPL + +extern inline void +list_node_link(struct list_node *node, struct list_node *prev, struct list_node *next); + +extern inline struct list_node * +list_node_unlink(struct list_node *node); + +extern inline void +list_push_head(struct list_node *restrict list, struct list_node *restrict node); + +extern inline void +list_push_tail(struct list_node *restrict list, struct list_node *restrict node); + +extern inline struct list_node * +list_pop_head(struct list_node *list); + +extern inline struct list_node * +list_pop_tail(struct list_node *list); + +#endif /* HEADER_IMPL */ diff --git a/pak.c b/pak.c @@ -0,0 +1,745 @@ +#define HEADER_IMPL + +#include "pak.h" + +#include <string.h> +#include <unistd.h> +#include <inttypes.h> + +/* mem stream helper + * =========================================================================== + */ + +struct pak_mem_stream { + uint8_t *buf, *end, *cur; +}; + +static inline size_t +pak_mem_stream_bytes_remaining(struct pak_mem_stream const *stream) +{ + assert(stream->cur <= stream->end); + return stream->end - stream->cur; +} + +static inline size_t +pak_mem_stream_bytes_consumed(struct pak_mem_stream const *stream) +{ + assert(stream->buf <= stream->cur); + return stream->cur - stream->buf; +} + +static inline void +pak_mem_stream_reset(struct pak_mem_stream *stream) +{ + stream->cur = stream->buf; +} + +static inline void * +pak_mem_stream_skip(struct pak_mem_stream *stream, size_t bytes) +{ + assert(stream->cur + bytes <= stream->end); + void *ptr = stream->cur; + stream->cur += bytes; + return ptr; +} + +static inline void +pak_mem_stream_readu8(struct pak_mem_stream *stream, uint8_t *out) +{ + assert(stream->cur + sizeof(uint8_t) <= stream->end); + uint8_t res = *(stream->cur++); + *out = res; + stream->cur += sizeof *out; +} + +static inline void +pak_mem_stream_readu16(struct pak_mem_stream *stream, uint16_t *out) +{ + assert(stream->cur + sizeof(uint16_t) <= stream->end); + uint16_t res = *((uint16_t *) stream->cur); + *out = res; + stream->cur += sizeof *out; +} + +static inline void +pak_mem_stream_readu32(struct pak_mem_stream *stream, uint32_t *out) +{ + assert(stream->cur + sizeof(uint32_t) <= stream->end); + uint32_t res = *((uint32_t *) stream->cur); + *out = res; + stream->cur += sizeof *out; +} + +static inline void +pak_mem_stream_readu64(struct pak_mem_stream *stream, uint64_t *out) +{ + assert(stream->cur + sizeof(uint64_t) <= stream->end); + uint64_t res = *((uint64_t *) stream->cur); + *out = res; + stream->cur += sizeof *out; +} + +static inline void +pak_mem_stream_readbuf(struct pak_mem_stream *stream, void *buf, size_t len) +{ + assert(stream->cur + len <= stream->end); + memcpy(buf, stream->cur, len); + stream->cur += len; +} + +#define PAK_MEM_STREAM_READ(stream, var) \ + _Generic((var), \ + uint8_t: pak_mem_stream_readu8, \ + uint16_t: pak_mem_stream_readu16, \ + uint32_t: pak_mem_stream_readu32, \ + uint64_t: pak_mem_stream_readu64 \ + )((stream), &(var)) + +static inline void +pak_mem_stream_writeu8(struct pak_mem_stream *stream, uint8_t val) +{ + assert(stream->cur + sizeof(uint8_t) <= stream->end); + *(stream->cur++) = val; + stream->cur += sizeof val; +} + +static inline void +pak_mem_stream_writeu16(struct pak_mem_stream *stream, uint16_t val) +{ + assert(stream->cur + sizeof(uint16_t) <= stream->end); + *((uint16_t *) stream->cur) = val; + stream->cur += sizeof val; +} + +static inline void +pak_mem_stream_writeu32(struct pak_mem_stream *stream, uint32_t val) +{ + assert(stream->cur + sizeof(uint32_t) <= stream->end); + *((uint32_t *) stream->cur) = val; + stream->cur += sizeof val; +} + +static inline void +pak_mem_stream_writeu64(struct pak_mem_stream *stream, uint64_t val) +{ + assert(stream->cur + sizeof(uint64_t) <= stream->end); + *((uint64_t *) stream->cur) = val; + stream->cur += sizeof val; +} + +static inline void +pak_mem_stream_writebuf(struct pak_mem_stream *stream, void const *buf, size_t len) +{ + assert(stream->cur + len <= stream->end); + memcpy(stream->cur, buf, len); + stream->cur += len; +} + +#define PAK_MEM_STREAM_WRITE(stream, var) \ + _Generic((var), \ + uint8_t: pak_mem_stream_writeu8, \ + uint16_t: pak_mem_stream_writeu16, \ + uint32_t: pak_mem_stream_writeu32, \ + uint64_t: pak_mem_stream_writeu64 \ + )((stream), (var)) + +/* =========================================================================== + */ + +static int +pak_write_mem_header(struct pak_mem_stream *stream, struct pakfmt_header const *hdr) +{ + if (pak_mem_stream_bytes_remaining(stream) < PAK_HEADER_SIZE) + return-1; + + pak_mem_stream_writebuf(stream, PAK_MAGIC, PAK_MAGIC_LEN); + PAK_MEM_STREAM_WRITE(stream, hdr->header_length); + + PAK_MEM_STREAM_WRITE(stream, hdr->index_file_offset); + PAK_MEM_STREAM_WRITE(stream, hdr->index_compression); + PAK_MEM_STREAM_WRITE(stream, hdr->index_compressed_length); + PAK_MEM_STREAM_WRITE(stream, hdr->index_uncompressed_length); + PAK_MEM_STREAM_WRITE(stream, hdr->index_uncompressed_crc32c); + + PAK_MEM_STREAM_WRITE(stream, hdr->data_file_offset); + PAK_MEM_STREAM_WRITE(stream, hdr->data_compressed_length); + + return 0; +} + +static int +pak_write_mem_tag(struct pak_mem_stream *stream, struct pak_tag const *tag) +{ + if (pak_mem_stream_bytes_remaining(stream) < PAK_TAG_SIZE) + return -1; + + pak_mem_stream_writebuf(stream, tag->header.name, sizeof tag->header.name); + PAK_MEM_STREAM_WRITE(stream, tag->header.type); + PAK_MEM_STREAM_WRITE(stream, tag->header.length); + + if (pak_mem_stream_bytes_remaining(stream) < tag->header.length) + return -1; + + pak_mem_stream_writebuf(stream, tag->data, tag->header.length); + + return 0; +} + +static int +pak_write_mem_archive(struct pak_mem_stream *stream, struct pak_archive const *archive) +{ + if (pak_mem_stream_bytes_remaining(stream) < PAK_ARCHIVE_SIZE) + return -1; + + pak_mem_stream_writebuf(stream, archive->header.name, sizeof archive->header.name); + PAK_MEM_STREAM_WRITE(stream, archive->header.type); + PAK_MEM_STREAM_WRITE(stream, archive->header.length); + + size_t archive_bytes_remaining = archive->header.length - PAK_ARCHIVE_PRELUDE_SIZE; + if (pak_mem_stream_bytes_remaining(stream) < archive_bytes_remaining) + return -1; + + PAK_MEM_STREAM_WRITE(stream, archive->header.data_offset); + PAK_MEM_STREAM_WRITE(stream, archive->header.data_compressed_length); + PAK_MEM_STREAM_WRITE(stream, archive->header.data_uncompressed_length); + PAK_MEM_STREAM_WRITE(stream, archive->header.data_uncompressed_crc32c); + PAK_MEM_STREAM_WRITE(stream, archive->header.data_compression); + + PAK_MEM_STREAM_WRITE(stream, archive->header.reserved); + PAK_MEM_STREAM_WRITE(stream, archive->header.tag_count); + + struct pak_tag *tag; + LIST_ENTRY_ITER(&archive->tags, tag, list_node) { + if (pak_write_mem_tag(stream, tag) < 0) + return -1; + } + + return 0; +} + +static int +pak_write_mem_archive_data(struct pak_mem_stream *stream, struct pak_archive const *archive) +{ + if (pak_mem_stream_bytes_remaining(stream) < archive->header.data_offset) + return -1; + + pak_mem_stream_skip(stream, archive->header.data_offset); + + assert(archive->header.data_compression == PAK_COMPRESSION_NONE); + assert(archive->header.data_compressed_length == archive->header.data_uncompressed_length); + + if (pak_mem_stream_bytes_remaining(stream) < archive->header.data_compressed_length) + return -1; + + pak_mem_stream_writebuf(stream, archive->data, archive->header.data_compressed_length); + + return 0; +} + +static int +pak_write_mem_index(struct pak_mem_stream *stream, struct list_node const *index) +{ + struct pak_archive *it; + LIST_ENTRY_ITER(index, it, list_node) { + if (pak_write_mem_archive(stream, it) < 0) + return -1; + } + + return 0; +} + +static int +pak_read_mem_header(struct pak_mem_stream *stream, struct pakfmt_header *hdr) +{ + if (pak_mem_stream_bytes_remaining(stream) < PAK_HEADER_SIZE) + return -1; + + pak_mem_stream_readbuf(stream, hdr->magic, sizeof hdr->magic); + if (strncmp(PAK_MAGIC, hdr->magic, sizeof hdr->magic)) + return -1; + + PAK_MEM_STREAM_READ(stream, hdr->header_length); + if (hdr->header_length != PAK_HEADER_SIZE) + return -1; + + PAK_MEM_STREAM_READ(stream, hdr->index_file_offset); + PAK_MEM_STREAM_READ(stream, hdr->index_compression); + PAK_MEM_STREAM_READ(stream, hdr->index_compressed_length); + PAK_MEM_STREAM_READ(stream, hdr->index_uncompressed_length); + PAK_MEM_STREAM_READ(stream, hdr->index_uncompressed_crc32c); + + PAK_MEM_STREAM_READ(stream, hdr->data_file_offset); + PAK_MEM_STREAM_READ(stream, hdr->data_compressed_length); + + return 0; +} + +static struct pak_tag * +pak_read_mem_tag(struct pak_mem_stream *stream, struct arena *arena) +{ + struct pak_tag *res = ARENA_ALLOC_SIZED(arena, struct pak_tag); + if (!res) return NULL; + + memset(res, 0, sizeof *res); + + if (pak_mem_stream_bytes_remaining(stream) < PAK_TAG_SIZE) + return NULL; + + pak_mem_stream_readbuf(stream, res->header.name, sizeof res->header.name); + PAK_MEM_STREAM_READ(stream, res->header.type); + PAK_MEM_STREAM_READ(stream, res->header.length); + + if (pak_mem_stream_bytes_remaining(stream) < res->header.length) + return NULL; + + res->data = pak_mem_stream_skip(stream, res->header.length); + + return res; +} + +static struct pak_archive * +pak_read_mem_archive(struct pak_mem_stream *stream, struct arena *arena) +{ + struct pak_archive *res = ARENA_ALLOC_SIZED(arena, struct pak_archive); + if (!res) return NULL; + + memset(res, 0, sizeof *res); + + if (pak_mem_stream_bytes_remaining(stream) < PAK_ARCHIVE_SIZE) + return NULL; + + pak_mem_stream_readbuf(stream, res->header.name, sizeof res->header.name); + PAK_MEM_STREAM_READ(stream, res->header.type); + PAK_MEM_STREAM_READ(stream, res->header.length); + + size_t archive_bytes_remaining = res->header.length - PAK_ARCHIVE_PRELUDE_SIZE; + if (pak_mem_stream_bytes_remaining(stream) < archive_bytes_remaining) + return NULL; + + PAK_MEM_STREAM_READ(stream, res->header.data_offset); + PAK_MEM_STREAM_READ(stream, res->header.data_compressed_length); + PAK_MEM_STREAM_READ(stream, res->header.data_uncompressed_length); + PAK_MEM_STREAM_READ(stream, res->header.data_uncompressed_crc32c); + PAK_MEM_STREAM_READ(stream, res->header.data_compression); + + PAK_MEM_STREAM_READ(stream, res->header.reserved); + PAK_MEM_STREAM_READ(stream, res->header.tag_count); + + res->tags = LIST_INIT(res->tags); + for (uint32_t i = 0; i < res->header.tag_count; i++) { + struct pak_tag *ent = pak_read_mem_tag(stream, arena); + if (!ent) return NULL; + + list_push_tail(&res->tags, &ent->list_node); + } + + return res; +} + +static int +pak_read_mem_archive_data(struct pak_mem_stream *stream, struct pak_archive *archive) +{ + if (pak_mem_stream_bytes_remaining(stream) < archive->header.data_offset) + return -1; + + pak_mem_stream_skip(stream, archive->header.data_offset); + + if (pak_mem_stream_bytes_remaining(stream) < archive->header.data_compressed_length) + return -1; + + archive->data = pak_mem_stream_skip(stream, archive->header.data_compressed_length); + + return 0; +} + +static int +pak_read_mem_index(struct pak_mem_stream *stream, struct arena *arena, struct list_node *index) +{ + while (pak_mem_stream_bytes_remaining(stream)) { + struct pak_archive *ent = pak_read_mem_archive(stream, arena); + if (!ent) return -1; + + list_push_tail(index, &ent->list_node); + } + + return 0; +} + +/* =========================================================================== + */ + +int +pak_read_header(int fd, struct pakfmt_header *out) +{ + if (lseek(fd, 0, SEEK_SET) < 0) + return -1; + + ssize_t bytes_read = 0; + if ((bytes_read = read(fd, out, sizeof *out)) < 0) + return -1; + + if ((size_t) bytes_read < sizeof *out) + return -1; + + return 0; +} + +int +pak_read_index(int fd, struct arena *arena, struct pakfmt_header const *hdr, + uint8_t *buf, size_t len, struct list_node *index) +{ + /* TODO: this assumes we can always do in-place decompression. + * is this the case? + */ + assert(hdr->index_uncompressed_length <= len); + + int res = -1; + + if (lseek(fd, hdr->index_file_offset, SEEK_SET) < 0) + goto error; + + ssize_t bytes_read = 0; + if ((bytes_read = read(fd, buf, hdr->index_compressed_length)) < 0) + goto error; + + if ((size_t) bytes_read < hdr->index_compressed_length) + goto error; + + /* TODO: implement decompression */ + assert(hdr->index_compression == PAK_COMPRESSION_NONE); + assert(hdr->index_compressed_length == hdr->index_uncompressed_length); + + struct pak_mem_stream stream = { + .buf = buf, .end = buf + hdr->index_uncompressed_length, .cur = buf, + }; + + res = pak_read_mem_index(&stream, arena, index); + +error: + return res; +} + +int +pak_read_archive(int fd, struct arena *arena, struct pakfmt_header const *hdr, + char name[static PAK_ARCHIVE_NAME_LEN], uint32_t type, + struct pak_archive **out) +{ + uint8_t prelude[PAK_ARCHIVE_PRELUDE_SIZE]; + + int res = -1; + + if (lseek(fd, hdr->index_file_offset, SEEK_SET) < 0) + goto error; + + do { + ssize_t bytes_read = 0; + if ((bytes_read = read(fd, prelude, sizeof prelude)) < 0) + goto error; + + if ((size_t) bytes_read < sizeof prelude) + goto error; + + // we want to simply access the first few fields in the + // archive (the "prelude"), and so casting is fine as long + // as we don't overrun our prelude buffer + struct pakfmt_archive *archive = (struct pakfmt_archive *) prelude; + size_t archive_bytes_remaining = archive->length - PAK_ARCHIVE_PRELUDE_SIZE; + + if (strncmp(name, archive->name, sizeof archive->name)) + goto skip; + + if (archive->type != type) + goto skip; + + // read rest of archive + uint8_t *buf = arena_alloc(arena, archive->length, alignof(struct pak_archive)); + if (!buf) + goto error; + + lseek(fd, -PAK_ARCHIVE_PRELUDE_SIZE, SEEK_CUR); + + if ((bytes_read = read(fd, buf, archive->length)) < 0) + goto error; + + if ((size_t) bytes_read < archive->length) + goto error; + + struct pak_mem_stream stream = { + .buf = buf, .end = buf + archive->length, .cur = buf, + }; + + *out = pak_read_mem_archive(&stream, arena); + if (!*out) + goto error; + + res = 0; + + break; + +skip: + if (lseek(fd, archive_bytes_remaining, SEEK_CUR) < 0) + goto error; + } while (1); + +error: + return res; +} + +int +pak_read_archive_data(int fd, struct pakfmt_header const *hdr, + struct pakfmt_archive const *archive, + uint8_t *buf, size_t len) +{ + assert(archive->data_compressed_length <= len); + + if (lseek(fd, hdr->data_file_offset + archive->data_offset, SEEK_SET) < 0) + return -1; + + ssize_t bytes_read = 0; + if ((bytes_read = read(fd, buf, archive->data_compressed_length)) < 0) + return -1; + + if ((size_t) bytes_read < archive->data_compressed_length) + return -1; + + return 0; +} + +int +pak_write(int fd, struct pak const *pak) +{ + int res = -1; + void *buf = NULL; + + size_t serialised_size = pak_file_size(pak); + buf = malloc(serialised_size); + if (!buf) + goto error; + + if (pak_write_mem(buf, serialised_size, pak) < 0) + goto error; + + size_t written = write(fd, buf, serialised_size); + if (written < serialised_size) + goto error; + + res = 0; + +error: + free(buf); + + return res; +} + +int +pak_write_mem(uint8_t *buf, size_t len, struct pak const *pak) +{ + struct pak_mem_stream stream = { + .buf = buf, .end = buf + len, .cur = buf, + }; + + if (pak_write_mem_header(&stream, &pak->header) < 0) + return -1; + + uint8_t *index_buf = buf + pak->header.index_file_offset; + uint8_t *index_buf_end = index_buf + pak->header.index_compressed_length; + uint8_t *data_buf = buf + pak->header.data_file_offset; + uint8_t *data_buf_end = data_buf + pak->header.data_compressed_length; + + struct pak_mem_stream index_stream = { + .buf = index_buf, .end = index_buf_end, .cur = index_buf, + }; + + if (pak_write_mem_index(&index_stream, &pak->index) < 0) + return -1; + + struct pak_mem_stream data_stream = { + .buf = data_buf, .end = data_buf_end, .cur = data_buf, + }; + + struct pak_archive *it; + LIST_ENTRY_ITER(&pak->index, it, list_node) { + pak_mem_stream_reset(&data_stream); + if (pak_write_mem_archive_data(&data_stream, it) < 0) + return -1; + } + + return 0; +} + +int +pak_read_mem(uint8_t *buf, size_t len, struct arena *arena, struct pak *pak) +{ + struct pak_mem_stream stream = { + .buf = buf, .end = buf + len, .cur = buf, + }; + + if (pak_read_mem_header(&stream, &pak->header) < 0) + return -1; + + uint8_t *buf_end = buf + len; + uint8_t *index_buf = buf + pak->header.index_file_offset; + uint8_t *index_buf_end = index_buf + pak->header.index_compressed_length; + uint8_t *data_buf = buf + pak->header.data_file_offset; + uint8_t *data_buf_end = data_buf + pak->header.data_compressed_length; + + if (index_buf_end > buf_end || data_buf_end > buf_end) + return -1; + + pak->index = LIST_INIT(pak->index); + + struct pak_mem_stream index_stream = { + .buf = index_buf, .end = index_buf_end, .cur = index_buf, + }; + + if (pak_read_mem_index(&index_stream, arena, &pak->index) < 0) + return -1; + + struct pak_mem_stream data_stream = { + .buf = data_buf, .end = data_buf_end, .cur = data_buf, + }; + + struct pak_archive *it; + LIST_ENTRY_ITER(&pak->index, it, list_node) { + pak_mem_stream_reset(&data_stream); + if (pak_read_mem_archive_data(&data_stream, it)) + return -1; + } + + return 0; +} + +void +pak_open(struct pak *pak) +{ + memcpy(pak->header.magic, PAK_MAGIC, sizeof pak->header.magic); + pak->header.header_length = sizeof pak->header; + + pak->header.index_file_offset = 0; + pak->header.index_compression = PAK_COMPRESSION_NONE; + pak->header.index_compressed_length = 0; + pak->header.index_uncompressed_length = 0; + pak->header.index_uncompressed_crc32c = 0; + + pak->header.data_file_offset = 0; + pak->header.data_compressed_length = 0; + + pak->index = LIST_INIT(pak->index); +} + +struct pak_archive * +pak_add_archive(struct pak *pak, struct arena *arena, + char name[static PAK_ARCHIVE_NAME_LEN], uint32_t type, + enum pak_compression_type compression, + struct pak_tag *tags, size_t tags_count, + void *data, size_t compressed_length, + size_t uncompressed_length, uint32_t uncompressed_crc32c) +{ + struct pak_archive *archive = ARENA_ALLOC_SIZED(arena, struct pak_archive); + if (!archive) return NULL; + + memset(archive, 0, sizeof *archive); + + memcpy(archive->header.name, name, sizeof archive->header.name); + archive->header.type = type; + archive->header.length = PAK_ARCHIVE_SIZE; + + archive->header.data_compressed_length = compressed_length; + archive->header.data_uncompressed_length = uncompressed_length; + archive->header.data_uncompressed_crc32c = uncompressed_crc32c; + archive->header.data_compression = compression; + + archive->header.tag_count = tags_count; + + archive->tags = LIST_INIT(archive->tags); + for (size_t i = 0; i < tags_count; i++) { + struct pak_tag *tag = ARENA_ALLOC_SIZED(arena, struct pak_tag); + if (!tag) return NULL; + + memset(tag, 0, sizeof *tag); + + memcpy(tag->header.name, tags[i].header.name, sizeof tag->header.name); + tag->header.type = tags[i].header.type; + tag->header.length = tags[i].header.length; + + tag->data = tags[i].data; + + list_push_tail(&archive->tags, &tag->list_node); + + archive->header.length += PAK_TAG_SIZE + tag->header.length; + } + + archive->data = data; + + list_push_tail(&pak->index, &archive->list_node); + + return archive; +} + +void +pak_close(struct pak *pak) +{ + pak->header.index_file_offset = pak->header.header_length; + + pak->header.index_compressed_length = 0; + pak->header.data_compressed_length = 0; + + struct pak_archive *it; + LIST_ENTRY_ITER(&pak->index, it, list_node) { + pak->header.index_uncompressed_length += it->header.length; + pak->header.index_uncompressed_crc32c = 0; /* TODO: calculate crc32c */ + + it->header.data_offset = pak->header.data_compressed_length; + pak->header.data_compressed_length += it->header.data_compressed_length; + } + + assert(pak->header.index_compression == PAK_COMPRESSION_NONE); /* TODO: index compression */ + pak->header.index_compressed_length = pak->header.index_uncompressed_length; + pak->header.data_file_offset = pak->header.index_file_offset + pak->header.index_compressed_length; +} + +void +pak_debug_dump(struct pak const *pak) +{ + printf("pak header:\n"); + printf("\tlength: %" PRIu32 "\n", pak->header.header_length); + printf("\tindex file offset: %" PRIu64 "\n", pak->header.index_file_offset); + printf("\tindex compression: %" PRIu32 "\n", pak->header.index_compression); + printf("\tindex compressed length: %" PRIu32 "\n", pak->header.index_compressed_length); + printf("\tindex uncompressed length: %" PRIu32 "\n", pak->header.index_uncompressed_length); + printf("\tindex uncompressed CRC32c: %" PRIu32 "\n", pak->header.index_uncompressed_crc32c); + printf("\tdata file offset: %" PRIu64 "\n", pak->header.data_file_offset); + printf("\tdata compressed length: %" PRIu64 "\n", pak->header.data_compressed_length); + + printf("pak index:\n"); + + size_t i = 0; + + struct pak_archive *it; + LIST_ENTRY_ITER(&pak->index, it, list_node) { + printf("\tarchive %zu:\n", i++); + printf("\t\tname: \"%.*s\"\n", (int) sizeof it->header.name, it->header.name); + printf("\t\tarchive type: %" PRIu32 "\n", it->header.type); + printf("\t\tarchive length: %" PRIu32 "\n", it->header.length); + printf("\t\tdata offset: %" PRIu64 "\n", it->header.data_offset); + printf("\t\tdata compressed length: %" PRIu64 "\n", it->header.data_compressed_length); + printf("\t\tdata uncompressed length: %" PRIu64 "\n", it->header.data_uncompressed_length); + printf("\t\tdata uncompressed CRC32c: %" PRIu32 "\n", it->header.data_uncompressed_crc32c); + printf("\t\tdata compression: %" PRIu32 "\n", it->header.data_compression); + printf("\t\ttag count: %" PRIu32 "\n", it->header.tag_count); + + printf("\t\tarchive tags:\n"); + + size_t j = 0; + + struct pak_tag *tag; + LIST_ENTRY_ITER(&it->tags, tag, list_node) { + printf("\t\t\ttag %zu:\n", j++); + printf("\t\t\t\ttag name: \"%.*s\"\n", (int) sizeof tag->header.name, tag->header.name); + printf("\t\t\t\ttag type: %" PRIu16 "\n", tag->header.type); + printf("\t\t\t\ttag length: %" PRIu16 "\n", tag->header.length); + } + + printf("\t\tarchive data: %p\n", it->data); + } +} diff --git a/pak.h b/pak.h @@ -0,0 +1,59 @@ +#ifndef PAK_H +#define PAK_H + +#include "alloc.h" +#include "list.h" +#include "pakfmt.h" + +static inline size_t +pak_file_size(struct pak const *pak) +{ + size_t index_end = pak->header.index_file_offset + pak->header.index_compressed_length; + size_t data_end = pak->header.data_file_offset + pak->header.data_compressed_length; + return (index_end < data_end) ? data_end : index_end; +} + +extern int +pak_read_header(int fd, struct pakfmt_header *out); + +extern int +pak_read_index(int fd, struct arena *arena, struct pakfmt_header const *hdr, + uint8_t *buf, size_t len, struct list_node *index); + +extern int +pak_read_archive(int fd, struct arena *arena, struct pakfmt_header const *hdr, + char name[PAK_ARCHIVE_NAME_LEN], uint32_t type, + struct pak_archive **out); + +extern int +pak_read_archive_data(int fd, struct pakfmt_header const *hdr, + struct pakfmt_archive const *archive, + uint8_t *buf, size_t len); + +extern int +pak_write(int fd, struct pak const *pak); + +extern int +pak_write_mem(uint8_t *buf, size_t len, struct pak const *pak); + +extern int +pak_read_mem(uint8_t *buf, size_t len, struct arena *arena, struct pak *pak); + +extern void +pak_open(struct pak *pak); + +extern struct pak_archive * +pak_add_archive(struct pak *pak, struct arena *arena, + char name[static PAK_ARCHIVE_NAME_LEN], uint32_t type, + enum pak_compression_type compression, + struct pak_tag *tags, size_t tags_count, + void *data, size_t compressed_length, size_t uncompressed_length, + uint32_t uncompressed_crc32c); + +extern void +pak_close(struct pak *pak); + +extern void +pak_debug_dump(struct pak const *pak); + +#endif /* PAK_H */ diff --git a/pakfmt.h b/pakfmt.h @@ -0,0 +1,205 @@ +#ifndef PAKFMT_H +#define PAKFMT_H + +#include <stddef.h> +#include <stdint.h> + +#include "list.h" + +/* Our PAK0 format: + * --- + * NOTE: the format is little-endian, so will not work naively across + * platforms with varying endianness; natively generated pak files + * will always be readable, but a pak file generated by a naive + * big-endian host will not be readable by a naive little-endian host. + * + * NOTE: the on-disk format follows C member alignment rules, to allow reading + * and writing the format easily (via memcpy() or similar). this may + * leave small "reserved" fields in certain structures to ensure alignment, + * and the specific value of these fields MUST be ignored by implementations. + * + * NOTE: the format limits the index segement to 4 GiB in size (by using a u32 + * field for storing the uncompressed index size). it does not however + * limit the size of the data segment (by using a u64 field for storing + * the uncompressed data size). this is deemed acceptable, as each + * archive entry should contain no more than a small archive header (80 + * bytes), and a variable, user-defined number of tags (again of + * variable, user-defined sizes). + * + * if you have too many archives for a single pak file, you likely + * contain hundreds of thousands of archives (each with tens of + * thousands of tags), at which point the suggestion is to split the + * pak; parsing such a large index is guaranteed to be slow, so by + * necessity you will want to split it on a per-stage / per-level basis. + * + * NOTE: the index can either preceede the data, or follow it. preceeding the + * data makes the format simpler to understand and parse (you can naively + * load the first N bytes instead of seeking to the end and rewinding), + * but following the data means that the contents can be easily modified + * (by stripping the index, appending to the data segment, and rewriting + * the index). this is a quality-of-implementation issue, and not + * specified by the format. + * + * Compression type enum + * +---------+ + * | None: 0 | + * +---------+ + * | LZ4: 1 | + * +---------+ + * + * Header Format + * +--------------------------------+---------------------------------------+ + * | Magic Value: char[4], "PAK0" | | + * +--------------------------------+---------------------------------------+ + * | Header Length: u32 | Byte length of header | + * +--------------------------------+---------------------------------------+ + * | Index File Offset: u64 | | + * +--------------------------------+---------------------------------------+ + * | Index Compression: u32 | | + * +--------------------------------+---------------------------------------+ + * | Index Compressed Length: u32 | | + * +--------------------------------+---------------------------------------+ + * | Index Uncompressed Length: u32 | | + * +--------------------------------+---------------------------------------+ + * | Index Uncompressed CRC32c: u32 | | + * +--------------------------------+---------------------------------------+ + * | Data File Offset: u64 | | + * +--------------------------------+---------------------------------------+ + * | Data Compressed Length: u64 | | + * +--------------------------------+---------------------------------------+ + * + * Index Format + * +--------------------------+---------------------------------------------+ + * | Index Entries: Archive[] | Array of archives contained in pak file | + * +--------------------------+---------------------------------------------+ + * + * Archive Format + * +-----------------------------------+------------------------------------+ + * | Archive Name: char[32] | Short, human readable name | + * +-----------------------------------+------------------------------------+ + * | Archive Type: u32 | User-defined archive type ID | + * +-----------------------------------+------------------------------------+ + * | Archive Length: u32 | Length of archive header + tags | + * +-----------------------------------+------------------------------------+ + * | Data Offset: u64 | Byte offset of archive data from | + * | | start of data segment | + * +-----------------------------------+------------------------------------+ + * | Data Compressed Length: u64 | | + * +-----------------------------------+------------------------------------+ + * | Data Uncompressed Length: u64 | | + * +-----------------------------------+------------------------------------+ + * | Data Uncompressed CRC32c: u32 | | + * +-----------------------------------+------------------------------------+ + * | Data Compression: u32 | | + * +-----------------------------------+------------------------------------+ + * | Reserved: u32 | | + * +-----------------------------------+------------------------------------+ + * | Tag Count: u32 | Number of metadata tags | + * +-----------------------------------+------------------------------------+ + * | Tags: Tag[] | | + * +-----------------------------------+------------------------------------+ + * + * Tag Format + * +-------------------------------------+----------------------------------+ + * | Tag Name: char[32] | Short, human readable name | + * +-------------------------------------+----------------------------------+ + * | Tag Type: u16 | User-defined tag type ID | + * +-------------------------------------+----------------------------------+ + * | Tag Length: u16 | Byte length of tag data | + * +-------------------------------------+----------------------------------+ + * | Tag Data: u8[] | | + * +-------------------------------------+----------------------------------+ + * + * Data Format + * +------------+-----------------------------------------------------------+ + * | Data: u8[] | Raw (compressed) contents of an archive | + * +------------+-----------------------------------------------------------+ + * + */ + +enum pak_compression_type { + PAK_COMPRESSION_NONE, + PAK_COMPRESSION_LZ4, +}; + +#define PAK_MAGIC "PAK0" +#define PAK_MAGIC_LEN 4 + +#define PAK_HEADER_PRELUDE_SIZE \ + (sizeof(char[PAK_MAGIC_LEN]) + sizeof(uint32_t)) + +#define PAK_HEADER_SIZE sizeof(struct pakfmt_header) + +struct pakfmt_header { + char magic[PAK_MAGIC_LEN]; + uint32_t header_length; + + uint64_t index_file_offset; + uint32_t index_compression; + uint32_t index_compressed_length; + uint32_t index_uncompressed_length; + uint32_t index_uncompressed_crc32c; + + uint64_t data_file_offset; + uint64_t data_compressed_length; +}; + +#define PAK_ARCHIVE_NAME_LEN 32 +#define PAK_ARCHIVE_PRELUDE_SIZE \ + (sizeof(char[PAK_ARCHIVE_NAME_LEN]) + sizeof(uint32_t) + sizeof(uint32_t)) + +#define PAK_ARCHIVE_SIZE sizeof(struct pakfmt_archive) + +struct pakfmt_archive { + char name[PAK_ARCHIVE_NAME_LEN]; + uint32_t type; + uint32_t length; + + uint64_t data_offset; + uint64_t data_compressed_length; + uint64_t data_uncompressed_length; + uint32_t data_uncompressed_crc32c; + uint32_t data_compression; + + uint32_t reserved; + + uint32_t tag_count; +}; + +#define PAK_TAG_NAME_LEN 32 +#define PAK_TAG_PRELUDE_SIZE \ + (sizeof(char[PAK_TAG_NAME_LEN]) + sizeof(uint16_t) + sizeof(uint16_t)) + +#define PAK_TAG_SIZE sizeof(struct pakfmt_tag) + +struct pakfmt_tag { + char name[PAK_TAG_NAME_LEN]; + uint16_t type; + uint16_t length; +}; + +struct pak_archive { + struct pakfmt_archive header; + + struct list_node tags; + + uint8_t *data; + + struct list_node list_node; +}; + +struct pak_tag { + struct pakfmt_tag header; + + uint8_t *data; + + struct list_node list_node; +}; + +struct pak { + struct pakfmt_header header; + + struct list_node index; +}; + +#endif /* PAKFMT_H */ diff --git a/test.c b/test.c @@ -0,0 +1,295 @@ +#define HEADER_IMPL + +#include "pak.h" + +#include <alloca.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <fcntl.h> +#include <sys/stat.h> + +#define TEST_PAK_FROM_MEM 1 +#define TEST_PAK_FROM_BUILDER 1 /* NOTE: TEST_PAK_FROM_MEM takes priority */ +#define TEST_PAK_READ_ALL 1 +#define TEST_PAK_READ_SINGLE_ARCHIVE 1 + +#define TEST_RAW_FILE "/tmp/test.txt" +#define TEST_PAK_FILE_IN "/tmp/test.pak" +#define TEST_PAK_FILE_OUT "/tmp/test2.pak" + +static char buf[8192]; + +static void +print_bytes(void *buf, size_t len, int format) +{ + uint8_t *ptr = buf; + + for (size_t i = 0; i < len; i++) { + if (format == 16) + printf("0x%x,", *ptr++); + else if (format == 2) { + uint8_t val = *ptr++; + printf("0b%u%u%u%u%u%u%u%u,", + !!(val & 0x80), + !!(val & 0x40), + !!(val & 0x20), + !!(val & 0x10), + !!(val & 0x8), + !!(val & 0x4), + !!(val & 0x2), + !!(val & 0x1)); + } else + printf("0d%u,", *ptr++); + } +} + +int +main(void) +{ + struct arena arena = { .ptr = buf, .cap = sizeof buf, .len = 0, }; + arena_reset(&arena); + + uint8_t test[] = { + /* header */ + 'P', 'A', 'K', '0', /* magic */ + 48, 0, 0, 0, /* header length */ + 48, 0, 0, 0, 0, 0, 0, 0, /* index file offset */ + 0, 0, 0, 0, /* index compression */ + 80 + 44, 0, 0, 0, /* index compressed length */ + 80 + 44, 0, 0, 0, /* index uncompressed length */ + 0, 0, 0, 0, /* index uncompressed crc32c */ + 48 + 80 + 44, 0, 0, 0, 0, 0, 0, 0, /* data file offset */ + 64, 0, 0, 0, 0, 0, 0, 0, /* data compressed length */ + + /* index */ + + /* archive 0: 80 bytes + sizeof tag0 */ + 'a','r','c','h','i','v','e','0', /* archive name */ + 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, + + 0, 0, 0, 0, /* archive type */ + + 80 + 44, 0, 0, 0, /* archive header + tags length */ + + 0, 0, 0, 0, 0, 0, 0, 0, /* data compressed offset */ + 64, 0, 0, 0, 0, 0, 0, 0, /* data compressed length */ + 64, 0, 0, 0, 0, 0, 0, 0, /* data uncompressed length */ + 0, 0, 0, 0, /* data uncompressed crc32c */ + 0, 0, 0, 0, /* data compression */ + + 0, 0, 0, 0, /* reserved */ + 1, 0, 0, 0, /* archive tag count */ + /* archive tags[] */ + + /* archive 0: tag 0: 36 bytes + sizeof tag0.data */ + 't','a','g','0',0,0,0,0, /* tag name */ + 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, + + 0, 0, /* tag type */ + 8, 0, /* tag length */ + /* tag data[] */ + 0,0,0,0,0,0,0,0, + + /* data */ + 0x0,0x1,0x2,0x3,0x4,0x5,0x6,0x7, 0x8,0x9,0xa,0xb,0xc,0xd,0xe,0xf, + 0x0,0x1,0x2,0x3,0x4,0x5,0x6,0x7, 0x8,0x9,0xa,0xb,0xc,0xd,0xe,0xf, + 0x0,0x1,0x2,0x3,0x4,0x5,0x6,0x7, 0x8,0x9,0xa,0xb,0xc,0xd,0xe,0xf, + 0x0,0x1,0x2,0x3,0x4,0x5,0x6,0x7, 0x8,0x9,0xa,0xb,0xc,0xd,0xe,0xf, + }; + + struct pak test_pak; + +#if TEST_PAK_FROM_MEM /* test pak deserialisation */ + + if (pak_read_mem(test, sizeof test, &arena, &test_pak) < 0) { + fprintf(stderr, "error: failed to read test pak from memory buffer\n"); + return -1; + } + + printf("in-memory test pak:\n"); + +#elif TEST_PAK_FROM_BUILDER /* test pak_open(), pak_add_archive(), and pak_close() */ + + pak_open(&test_pak); + + char name[PAK_ARCHIVE_NAME_LEN] = "archive0"; + uint32_t type = 0; + + uint8_t tag_data[] = { + 0,0,0,0,0,0,0,0, + }; + + struct pak_tag tag = { + .header = { .name = "tag0", .type = 0, .length = sizeof tag_data, }, + .data = tag_data, + }; + + uint8_t archive_data[] = { + 0x0,0x1,0x2,0x3,0x4,0x5,0x6,0x7, 0x8,0x9,0xa,0xb,0xc,0xd,0xe,0xf, + 0x0,0x1,0x2,0x3,0x4,0x5,0x6,0x7, 0x8,0x9,0xa,0xb,0xc,0xd,0xe,0xf, + 0x0,0x1,0x2,0x3,0x4,0x5,0x6,0x7, 0x8,0x9,0xa,0xb,0xc,0xd,0xe,0xf, + 0x0,0x1,0x2,0x3,0x4,0x5,0x6,0x7, 0x8,0x9,0xa,0xb,0xc,0xd,0xe,0xf, + }; + + struct pak_archive *archive = NULL; + if ((archive = pak_add_archive(&test_pak, &arena, name, type, + PAK_COMPRESSION_NONE, + &tag, 1, + archive_data, sizeof archive_data, + sizeof archive_data, 0)) == NULL) { + fprintf(stderr, "error: failed to add archive to pak file\n"); + return -1; + } + + pak_close(&test_pak); + + printf("builder test pak:\n"); + +#endif + + { + pak_debug_dump(&test_pak); + + size_t test_file_size = pak_file_size(&test_pak); + printf("test pak file serialised size: %zu\n", test_file_size); + + void *test_buf = alloca(test_file_size); + assert(test_buf); + + if (pak_write_mem(test_buf, test_file_size, &test_pak) < 0) { + fprintf(stderr, "error: failed to serialise pak file\n"); + return -1; + } + + assert(sizeof test == test_file_size); + assert(memcmp(test, test_buf, test_file_size) == 0); + + int fd = open(TEST_PAK_FILE_IN, O_WRONLY | O_CREAT, 0644); + if (fd < 0) { + fprintf(stderr, "error: failed to open test pak file: %s\n", TEST_PAK_FILE_IN); + return -1; + } + + size_t written = write(fd, test_buf, test_file_size); + assert(written == test_file_size); + } + +#if TEST_PAK_READ_ALL /* test whole-pak reading */ + + { + arena_reset(&arena); + + int in = open(TEST_PAK_FILE_IN, O_RDONLY); + if (in < 0) { + fprintf(stderr, "error: failed to open input pak file: %s\n", TEST_PAK_FILE_IN); + return -1; + } + + struct pak pak; + if (pak_read_header(in, &pak.header) < 0) { + fprintf(stderr, "error: failed to read pak file header: %s\n", TEST_PAK_FILE_IN); + return -1; + } + + size_t len = pak.header.index_uncompressed_length; + uint8_t *data = alloca(len); + + pak.index = LIST_INIT(pak.index); + if (pak_read_index(in, &arena, &pak.header, data, len, &pak.index) < 0) { + fprintf(stderr, "error: failed to read pak file index: %s\n", TEST_PAK_FILE_IN); + return -1; + } + + struct pak_archive *it; + LIST_ENTRY_ITER(&pak.index, it, list_node) { + size_t archive_len = it->header.data_uncompressed_length; + uint8_t *archive_data = arena_alloc(&arena, archive_len, 1); + assert(archive_data); + + if (pak_read_archive_data(in, &pak.header, &it->header, archive_data, archive_len) < 0) { + fprintf(stderr, "error: failed to read archive \"%.*s\" data from pak file: %s\n", + (int) sizeof it->header.name, it->header.name, TEST_PAK_FILE_IN); + return -1; + } + + it->data = archive_data; + } + + printf("read pak:\n"); + pak_debug_dump(&pak); + + int out = open(TEST_PAK_FILE_OUT, O_WRONLY | O_CREAT, 0644); + if (out < 0) { + fprintf(stderr, "error: failed to open output pak file: %s\n", TEST_PAK_FILE_OUT); + return -1; + } + + if (pak_write(out, &pak) < 0) { + fprintf(stderr, "error: failed to write pak file: %s\n", TEST_PAK_FILE_OUT); + return -1; + } + } + +#endif + +#if TEST_PAK_READ_SINGLE_ARCHIVE /* test pak single-archive reading */ + + { + int single = open(TEST_PAK_FILE_OUT, O_RDONLY); + assert(single >= 0); + + arena_reset(&arena); + + struct pakfmt_header header; + if (pak_read_header(single, &header) < 0) { + fprintf(stderr, "error: failed to read pak header from file: %s\n", + TEST_PAK_FILE_OUT); + return -1; + } + + struct pak_archive *archive; + char name[PAK_ARCHIVE_NAME_LEN] = "archive0"; + uint32_t type = 0; + + if (pak_read_archive(single, &arena, &header, name, type, &archive) < 0) { + fprintf(stderr, "error: failed to read archive \"%s\" from file: %s\n", + name, TEST_PAK_FILE_OUT); + return -1; + } + + size_t len = archive->header.data_uncompressed_length; + uint8_t *data = alloca(len); + if (pak_read_archive_data(single, &header, &archive->header, data, len) < 0) { + fprintf(stderr, "error: failed to read archive \"%s\" data from file: %s\n", + name, TEST_PAK_FILE_OUT); + return -1; + } + + printf("Archive data: %zu bytes\n", archive->header.data_uncompressed_length); + print_bytes(data, archive->header.data_uncompressed_length, 16); + printf("\n"); + + printf("Archive tags:\n"); + + struct pak_tag *it; + LIST_ENTRY_ITER(&archive->tags, it, list_node) { + printf("\ttag: \"%.*s\", type: %u\n", + (int) sizeof it->header.name, it->header.name, it->header.type); + print_bytes(it->data, it->header.length, 16); + printf("\n"); + } + } +#endif + + return 0; +} + +#include "pak.c"