prosperon/source/engine/qop.h

283 lines
7.2 KiB
C

/*
Copyright (c) 2024, Dominic Szablewski - https://phoboslab.org
SPDX-License-Identifier: MIT
QOP - The “Quite OK Package Format” for bare bones file packages
// Define `QOP_IMPLEMENTATION` in *one* C/C++ file before including this
// library to create the implementation.
#define QOP_IMPLEMENTATION
#include "qop.h"
-- File format description (pseudo code)
struct {
// Path string and data of all files in this archive
struct {
uint8_t path[path_len];
uint8_t bytes[size];
} file_data[];
// The index, with a list of files
struct {
uint64_t hash;
uint32_t offset;
uint32_t size;
uint16_t path_len;
uint16_t flags;
} qop_file[];
// The number of files in the index
uint32_t index_len;
// The size of the whole archive, including the header
uint32_t archive_size;
// Magic bytes "qopf"
uint32_t magic;
} qop;
*/
/* -----------------------------------------------------------------------------
Header - Public functions */
#ifndef QOP_H
#define QOP_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdio.h>
#include <string.h>
#define QOP_FLAG_NONE 0
#define QOP_FLAG_COMPRESSED_ZSTD (1 << 0)
#define QOP_FLAG_COMPRESSED_DEFLATE (1 << 1)
#define QOP_FLAG_ENCRYPTED (1 << 8)
typedef struct {
unsigned long long hash;
unsigned int offset;
unsigned int size;
unsigned short path_len;
unsigned short flags;
} qop_file;
typedef struct {
FILE *fh;
qop_file *hashmap;
unsigned int files_offset;
unsigned int index_offset;
unsigned int index_len;
unsigned int hashmap_len;
unsigned int hashmap_size;
} qop_desc;
// Open an archive at path. The supplied qop_desc will be filled with the
// information from the file header. Returns the size of the archvie or 0 on
// failure
int qop_open(const char *path, qop_desc *qop);
// Read the index from an opened archive. The supplied buffer will be filled
// with the index data and must be at least qop->hashmap_size bytes long.
// No ownership is taken of the buffer; if you allocated it with malloc() you
// need to free() it yourself after qop_close();
// Returns the number of files in the archive or 0 on error.
int qop_read_index(qop_desc *qop, void *buffer);
// Close the archive
void qop_close(qop_desc *qop);
// Find a file with the supplied path. Returns NULL if the file is not found
qop_file *qop_find(qop_desc *qop, const char *path);
// Copy the path of the file into dest. The dest buffer must be at least
// file->path_len bytes long. The path is null terminated.
// Returns the path length (including the null terminater) or 0 on error.
int qop_read_path(qop_desc *qop, qop_file *file, char *dest);
// Read the whole file into dest. The dest buffer must be at least file->size
// bytes long.
// Returns the number of bytes read
int qop_read(qop_desc *qop, qop_file *file, unsigned char *dest);
// Read part of a file into dest. The dest buffer must be at least len bytes
// long.
// Returns the number of bytes read.
int qop_read_ex(qop_desc *qop, qop_file *file, unsigned char *dest, unsigned int start, unsigned int len);
#ifdef __cplusplus
}
#endif
#endif /* QOP_H */
/* -----------------------------------------------------------------------------
Implementation */
#ifdef QOP_IMPLEMENTATION
typedef unsigned long long qop_uint64_t;
#define QOP_MAGIC \
(((unsigned int)'q') << 0 | ((unsigned int)'o') << 8 | \
((unsigned int)'p') << 16 | ((unsigned int)'f') << 24)
#define QOP_HEADER_SIZE 12
#define QOP_INDEX_SIZE 20
// MurmurOAAT64
static inline qop_uint64_t qop_hash(const char *key) {
qop_uint64_t h = 525201411107845655ull;
for (;*key;++key) {
h ^= (unsigned char)*key;
h *= 0x5bd1e9955bd1e995ull;
h ^= h >> 47;
}
return h;
}
static unsigned short qop_read_16(FILE *fh) {
unsigned char b[sizeof(unsigned short)] = {0};
if (fread(b, sizeof(unsigned short), 1, fh) != 1) {
return 0;
}
return (b[1] << 8) | b[0];
}
static unsigned int qop_read_32(FILE *fh) {
unsigned char b[sizeof(unsigned int)] = {0};
if (fread(b, sizeof(unsigned int), 1, fh) != 1) {
return 0;
}
return (b[3] << 24) | (b[2] << 16) | (b[1] << 8) | b[0];
}
static qop_uint64_t qop_read_64(FILE *fh) {
unsigned char b[sizeof(qop_uint64_t)] = {0};
if (fread(b, sizeof(qop_uint64_t), 1, fh) != 1) {
return 0;
}
return
((qop_uint64_t)b[7] << 56) | ((qop_uint64_t)b[6] << 48) |
((qop_uint64_t)b[5] << 40) | ((qop_uint64_t)b[4] << 32) |
((qop_uint64_t)b[3] << 24) | ((qop_uint64_t)b[2] << 16) |
((qop_uint64_t)b[1] << 8) | ((qop_uint64_t)b[0]);
}
int qop_open(const char *path, qop_desc *qop) {
FILE *fh = fopen(path, "rb");
if (!fh) {
return 0;
}
fseek(fh, 0, SEEK_END);
int size = ftell(fh);
if (size <= QOP_HEADER_SIZE || fseek(fh, size - QOP_HEADER_SIZE, SEEK_SET) != 0) {
fclose(fh);
return 0;
}
qop->fh = fh;
qop->hashmap = NULL;
unsigned int index_len = qop_read_32(fh);
unsigned int archive_size = qop_read_32(fh);
unsigned int magic = qop_read_32(fh);
// Check magic, make sure index_len is possible with the file size
if (
magic != QOP_MAGIC ||
index_len * QOP_INDEX_SIZE > (unsigned int)(size - QOP_HEADER_SIZE)
) {
fclose(fh);
return 0;
}
// Find a good size for the hashmap: power of 2, at least 1.5x num entries
unsigned int hashmap_len = 1;
unsigned int min_hashmap_len = index_len * 1.5;
while (hashmap_len < min_hashmap_len) {
hashmap_len <<= 1;
}
qop->files_offset = size - archive_size;
qop->index_len = index_len;
qop->index_offset = size - qop->index_len * QOP_INDEX_SIZE - QOP_HEADER_SIZE;
qop->hashmap_len = hashmap_len;
qop->hashmap_size = qop->hashmap_len * sizeof(qop_file);
return size;
}
int qop_read_index(qop_desc *qop, void *buffer) {
qop->hashmap = buffer;
int mask = qop->hashmap_len - 1;
memset(qop->hashmap, 0, qop->hashmap_size);
fseek(qop->fh, qop->index_offset, SEEK_SET);
for (unsigned int i = 0; i < qop->index_len; i++) {
qop_uint64_t hash = qop_read_64(qop->fh);
int idx = hash & mask;
while (qop->hashmap[idx].size > 0) {
idx = (idx + 1) & mask;
}
qop->hashmap[idx].hash = hash;
qop->hashmap[idx].offset = qop_read_32(qop->fh);
qop->hashmap[idx].size = qop_read_32(qop->fh);
qop->hashmap[idx].path_len = qop_read_16(qop->fh);
qop->hashmap[idx].flags = qop_read_16(qop->fh);
}
return qop->index_len;
}
void qop_close(qop_desc *qop) {
fclose(qop->fh);
}
qop_file *qop_find(qop_desc *qop, const char *path) {
if (qop->hashmap == NULL) {
return NULL;
}
int mask = qop->hashmap_len - 1;
qop_uint64_t hash = qop_hash(path);
int idx = hash & mask;
while (qop->hashmap[idx].size > 0) {
if (qop->hashmap[idx].hash == hash) {
return &qop->hashmap[idx];
}
idx = (idx + 1) & mask;
}
return NULL;
}
int qop_read_path(qop_desc *qop, qop_file *file, char *dest) {
fseek(qop->fh, qop->files_offset + file->offset, SEEK_SET);
return fread(dest, 1, file->path_len, qop->fh);
}
int qop_read(qop_desc *qop, qop_file *file, unsigned char *dest) {
fseek(qop->fh, qop->files_offset + file->offset + file->path_len, SEEK_SET);
return fread(dest, 1, file->size, qop->fh);
}
int qop_read_ex(qop_desc *qop, qop_file *file, unsigned char *dest, unsigned int start, unsigned int len) {
fseek(qop->fh, qop->files_offset + file->offset + file->path_len + start, SEEK_SET);
return fread(dest, 1, len, qop->fh);
}
#endif /* QOP_IMPLEMENTATION */