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 | +++ |
A | LICENSE | | | 18 | ++++++++++++++++++ |
A | README | | | 4 | ++++ |
A | alloc.h | | | 57 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | build.sh | | | 9 | +++++++++ |
A | clean.sh | | | 5 | +++++ |
A | list.h | | | 111 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | pak.c | | | 745 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | pak.h | | | 59 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | pakfmt.h | | | 205 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | test.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"