283 lines
7.2 KiB
C
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 */
|