commit 7571cfbca7f5161dbded5cccc6baf87e947d956b
Author: TopchetoEU <36534413+TopchetoEU@users.noreply.github.com>
Date: Sat Mar 8 23:22:10 2025 +0200
initial commit
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..bf7cc56
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,5 @@
+[*.?ts]
+end_of_line = lf
+indent_style = tab
+insert_final_newline = true
+trim_trailing_whitespace = true
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..cda1058
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+/*
+!/lib
+!/mod
+!/src
+!/.editorconfig
+!/.gitignore
+!/Makefile
+!/LICENSE
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5ce0119
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,15 @@
+Slimpack
+Copyright (C) 2025 TopchetoEU
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..79dea94
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,16 @@
+lua_sources = $(wildcard src/*.lua) $(wildcard src/cli/*.lua) $(wildcard src/formats/*.lua) $(wildcard src/util/*.lua)
+c_sources = lib/main.c lib/http.c lib/fmt.c lib/zlib.c
+target = dst/slimpack
+lua_target = dst/slimpack.h
+
+.PHONY: build
+
+build: $(target)
+
+./dst:
+ mkdir -p $@
+
+$(lua_target): $(lua_sources) ./dst
+ lua lib/build.lua src $@ $(lua_sources)
+$(target): $(c_sources) $(lua_target) ./dst
+ gcc -g -Wall -include $(lua_target) $(c_sources) -o $@ -llua -lcurl -lz -Wall
diff --git a/lib/build.lua b/lib/build.lua
new file mode 100644
index 0000000..3721754
--- /dev/null
+++ b/lib/build.lua
@@ -0,0 +1,50 @@
+local function unresolve_require(paths, f)
+ for _, pattern in ipairs(paths) do
+ local path = f:match(pattern);
+ if path then
+ return path:gsub("%/", ".");
+ end
+ end
+
+ error("path " .. f .. " couldn't be resolved to any valid module");
+end
+
+local function all_loader(paths, ...)
+ local files = { ... };
+ local function coro_entry()
+ for _, f in ipairs(files) do
+ local name = unresolve_require(paths, f);
+ local bytecode = string.dump(assert(load(io.lines(f, 1024), "@" .. f, "t", nil)));
+
+ coroutine.yield(("package.preload[%q] = load(%q, %q, 'b');"):format(name, bytecode, f));
+ end
+ coroutine.yield("require 'init' (...);");
+ end
+
+ return coroutine.wrap(coro_entry);
+end
+
+local function main(root, out, ...)
+ local paths = { root .. "/(.+).lua", root .. "/(.+)/init.lua" };
+
+ for el in package.path:gmatch "[^;]+" do
+ paths[#paths + 1] = el:gsub("?", "(.+)");
+ end
+
+ local res = string.dump(assert(load(all_loader(paths, ...), "@", "t", nil)));
+
+ local f = assert(io.open(out, "w"));
+ f:write "#include \n";
+ f:write "#define BYTECODE entry_bytecode\n";
+ f:write "static const uint8_t entry_bytecode[] = { ";
+
+ for i = 1, #res do
+ f:write(string.byte(res, i), ", ");
+ end
+
+ f:write "};";
+
+ assert(f:close());
+end
+
+main(...);
diff --git a/lib/fmt.c b/lib/fmt.c
new file mode 100644
index 0000000..a111c74
--- /dev/null
+++ b/lib/fmt.c
@@ -0,0 +1,771 @@
+// Similar in concept to Lua's pack and unpack
+// Main differences are:
+// - Some formats are reinterpreted to mean the same thing across architectures
+// - Default endianness is big-endian (but that can easily be changed, of course)
+// - Formats are parsed once and used many times, to reduce overhead
+// - Polyfills will store packers and unpackers in weak tables
+// - Default alignment is no alignment
+// - The maximum size of an integer is 64 bits (instead of 128)
+
+#include
+#include
+#include
+#include
+#include
+
+#include "lib.h"
+
+#define FMT_UDATA_NAME "fmt.meta"
+
+typedef enum {
+ FMT_INT8,
+ FMT_UINT8,
+ FMT_INT16,
+ FMT_UINT16,
+ FMT_INT32,
+ FMT_UINT32,
+ FMT_INT64,
+ FMT_UINT64,
+
+ FMT_FLOAT32,
+ FMT_FLOAT64,
+
+ FMT_STR_FIXED,
+ FMT_STR8,
+ FMT_STR16,
+ FMT_STR32,
+ FMT_STR64,
+ FMT_STR_ZERO,
+
+ FMT_PADDING,
+} fmt_type_t;
+
+typedef enum {
+ FMT_OK,
+ FMT_BAD_ARGS,
+ FMT_INCOMPLETE_OP,
+ FMT_BAD_OP_ARG,
+ FMT_BAD_OP,
+} fmt_code_t;
+
+typedef struct {
+ fmt_type_t type;
+ size_t size, align;
+} fmt_segment_t;
+
+typedef struct {
+ bool little_endian;
+ size_t max_align;
+ size_t segments_n;
+ fmt_segment_t segments[];
+} fmt_t;
+
+static int fmt_parse_width(size_t *i, const char *raw, size_t raw_size) {
+ if (*i < raw_size) {
+ switch (raw[(*i)++]) {
+ case '1': return 1;
+ case '2': return 2;
+ case '4': return 4;
+ case '8': return 8;
+ default:
+ (*i)--;
+ return 8;
+ }
+ }
+ else return 8;
+}
+
+static fmt_code_t fmt_parse_fmt(lua_State *ctx, const char *raw, size_t raw_size) {
+ if (raw == NULL || *raw == 0) return FMT_BAD_ARGS;
+ if (raw_size == -1) raw_size = strlen(raw);
+
+ fmt_segment_t segments[raw_size];
+ size_t segments_n = 0;
+ bool little_endian;
+ bool max_align;
+
+ size_t i = 0;
+ bool curr_padding = false;
+
+ while (true) {
+ if (i >= raw_size) break;
+ fmt_segment_t segment;
+
+ switch (raw[i++]) {
+ case '<': little_endian = true; continue;
+ case '>':
+ case '=': little_endian = false; continue;
+ case '!':
+ if (i < raw_size && (raw[i] < '0' || raw[i] > '9')) {
+ max_align = raw[i] - '0';
+ i++;
+ }
+ else max_align = 1;
+
+ continue;
+
+ case 'b': segment = (fmt_segment_t){ .type = FMT_INT8, .align = 1 }; break;
+ case 'B': segment = (fmt_segment_t){ .type = FMT_UINT8, .align = 1 }; break;
+ case 'h': segment = (fmt_segment_t){ .type = FMT_INT16, .align = 2 }; break;
+ case 'H': segment = (fmt_segment_t){ .type = FMT_UINT16, .align = 2 }; break;
+ case 'j': segment = (fmt_segment_t){ .type = FMT_INT32, .align = 4 }; break;
+ case 'J': segment = (fmt_segment_t){ .type = FMT_UINT32, .align = 4 }; break;
+ case 'l': segment = (fmt_segment_t){ .type = FMT_INT64, .align = 8 }; break;
+ case 'L':
+ case 'T': segment = (fmt_segment_t){ .type = FMT_UINT64, .align = 8 }; break;
+
+ case 'f': segment = (fmt_segment_t){ .type = FMT_FLOAT32, .align = 4 }; break;
+ case 'd':
+ case 'n': segment = (fmt_segment_t){ .type = FMT_FLOAT64, .align = 8 }; break;
+
+ case 'i':
+ switch (fmt_parse_width(&i, raw, raw_size)) {
+ case 1: segment = (fmt_segment_t){ .type = FMT_INT8, .align = 1 }; break;
+ case 2: segment = (fmt_segment_t){ .type = FMT_INT16, .align = 2 }; break;
+ case 4: segment = (fmt_segment_t){ .type = FMT_INT32, .align = 4 }; break;
+ case 8: segment = (fmt_segment_t){ .type = FMT_INT64, .align = 8 }; break;
+ }
+ break;
+ case 'I':
+ switch (fmt_parse_width(&i, raw, raw_size)) {
+ case 1: segment = (fmt_segment_t){ .type = FMT_UINT8, .align = 1 }; break;
+ case 2: segment = (fmt_segment_t){ .type = FMT_UINT16, .align = 2 }; break;
+ case 4: segment = (fmt_segment_t){ .type = FMT_UINT32, .align = 4 }; break;
+ case 8: segment = (fmt_segment_t){ .type = FMT_UINT64, .align = 8 }; break;
+ }
+ break;
+
+ case 'z': segment = (fmt_segment_t){ .type = FMT_STR_ZERO, .align = 1 }; break;
+ case 'c':
+ bool has_size;
+ size_t str_size;
+
+ while (i >= raw_size && raw[i] >= '0' && raw[i] <= '9') {
+ str_size *= 10;
+ str_size += raw[i] - '0';
+ has_size = true;
+ i++;
+ }
+
+ if (!has_size) return FMT_INCOMPLETE_OP;
+
+ segment = (fmt_segment_t){ .type = FMT_STR_FIXED, .size = str_size, .align = 1 };
+ break;
+ case 's':
+ switch (fmt_parse_width(&i, raw, raw_size)) {
+ case 1: segment = (fmt_segment_t){ .type = FMT_STR8, .align = 1 }; break;
+ case 2: segment = (fmt_segment_t){ .type = FMT_STR16, .align = 2 }; break;
+ case 4: segment = (fmt_segment_t){ .type = FMT_STR32, .align = 4 }; break;
+ case 8: segment = (fmt_segment_t){ .type = FMT_STR64, .align = 8 }; break;
+ }
+ break;
+
+ case 'x': segment = (fmt_segment_t){ .type = FMT_PADDING, .size = 1, .align = 1 }; break;
+ case 'X':
+ curr_padding = true;
+ continue;
+ case ' ':
+ break;
+
+ default:
+ return FMT_BAD_OP;
+ }
+
+ if (curr_padding) {
+ // TODO: is this correct?
+ switch (segment.type) {
+ case FMT_INT8:
+ case FMT_UINT8:
+ case FMT_STR8:
+ segment = (fmt_segment_t){ .type = FMT_PADDING, .size = 1, .align = 1 };
+ break;
+ case FMT_INT16:
+ case FMT_UINT16:
+ case FMT_STR16:
+ segment = (fmt_segment_t){ .type = FMT_PADDING, .size = 2, .align = 2 };
+ break;
+ case FMT_INT32:
+ case FMT_UINT32:
+ case FMT_FLOAT32:
+ case FMT_STR32:
+ segment = (fmt_segment_t){ .type = FMT_PADDING, .size = 4, .align = 4 };
+ break;
+ case FMT_INT64:
+ case FMT_UINT64:
+ case FMT_FLOAT64:
+ case FMT_STR64:
+ segment = (fmt_segment_t){ .type = FMT_PADDING, .size = 8, .align = 8 };
+ break;
+ case FMT_STR_FIXED:
+ segment = (fmt_segment_t){ .type = FMT_PADDING, .size = segment.size, .align = segment.size };
+ break;
+ default:
+ return FMT_BAD_OP_ARG;
+ }
+ }
+
+ segments[segments_n++] = segment;
+ continue;
+ }
+
+ if (curr_padding) return FMT_INCOMPLETE_OP;
+
+ for (size_t i = 0; i < segments_n; i++) {
+ // TODO: this might not be correct...
+ if (segments[i].align > max_align) segments[i].align = 8;
+ if (segments[i].align > max_align) segments[i].align = 4;
+ if (segments[i].align > max_align) segments[i].align = 2;
+ if (segments[i].align > max_align) segments[i].align = 1;
+ }
+
+ fmt_t *res = lua_newuserdata(ctx, sizeof *res + sizeof *segments * segments_n);
+ luaL_getmetatable(ctx, FMT_UDATA_NAME);
+ lua_setmetatable(ctx, -2);
+
+ res->little_endian = little_endian;
+ res->max_align = max_align;
+ res->segments_n = segments_n;
+ memcpy(res->segments, segments, sizeof *segments * segments_n);
+
+ return FMT_OK;
+}
+
+
+typedef struct {
+ char *arr;
+ size_t cap, n;
+} write_buff_t;
+
+static void write_buff_append(write_buff_t *buff, const char *data, size_t size) {
+ size_t new_cap = buff->cap;
+
+ while (new_cap < buff->n + size) {
+ if (new_cap == 0) new_cap = 16;
+ else new_cap *= 2;
+ }
+
+ if (new_cap != buff->cap) {
+ if (buff->cap == 0) {
+ buff->arr = malloc(new_cap);
+ }
+ else {
+ buff->arr = realloc(buff->arr, new_cap);
+ }
+ buff->cap = new_cap;
+ }
+
+ if (data == NULL) {
+ memset(buff->arr + buff->n, 0, size);
+ }
+ else {
+ memcpy(buff->arr + buff->n, data, size);
+ }
+
+ buff->n += size;
+}
+static void write_buff_fit(write_buff_t *buff) {
+ if (buff->arr == NULL) return;
+
+ if (buff->n == 0) {
+ free(buff->arr);
+ buff->cap = 0;
+ }
+ else {
+ buff->cap = buff->n;
+ buff->arr = realloc(buff->arr, buff->n);
+ }
+}
+
+
+static bool fmt_read_uint8(const uint8_t *raw, size_t *i, size_t size, uint8_t *res) {
+ if ((*i) + 1 > size || *i < 0) return false;
+
+ *res = raw[(*i)++];
+
+ return true;
+}
+static bool fmt_read_int8(const uint8_t *raw, size_t *i, size_t size, int8_t *res) {
+ return fmt_read_uint8(raw, i, size, (uint8_t*)res);
+}
+
+static bool fmt_read_uint16(const uint8_t *raw, size_t *i, size_t size, uint16_t *res, bool little_endian) {
+ if ((*i) + 2 > size || *i < 0) return false;
+
+ uint16_t a = raw[(*i)++];
+ uint16_t b = raw[(*i)++];
+
+ if (little_endian) *res = a | b << 8;
+ else *res = a << 8 | b;
+
+ return true;
+}
+static bool fmt_read_int16(const uint8_t *raw, size_t *i, size_t size, int16_t *res, bool little_endian) {
+ return fmt_read_uint16(raw, i, size, (uint16_t*)res, little_endian);
+}
+
+static bool fmt_read_uint32(const uint8_t *raw, size_t *i, size_t size, uint32_t *res, bool little_endian) {
+ if ((*i) + 4 > size || *i < 0) return false;
+
+ uint8_t a = raw[(*i)++];
+ uint8_t b = raw[(*i)++];
+ uint8_t c = raw[(*i)++];
+ uint8_t d = raw[(*i)++];
+
+ if (little_endian) *res = a | b << 8 | c << 16 | d << 24;
+ else *res = a << 24 | b << 16 | c << 8 | d;
+
+ return true;
+}
+static bool fmt_read_int32(const uint8_t *raw, size_t *i, size_t size, int32_t *res, bool little_endian) {
+ return fmt_read_uint32(raw, i, size, (uint32_t*)res, little_endian);
+}
+
+static bool fmt_read_uint64(const uint8_t *raw, size_t *i, size_t size, uint64_t *res, bool little_endian) {
+ if ((*i) + 8 > size || *i < 0) return false;
+
+ uint64_t a = raw[(*i)++];
+ uint64_t b = raw[(*i)++];
+ uint64_t c = raw[(*i)++];
+ uint64_t d = raw[(*i)++];
+ uint64_t e = raw[(*i)++];
+ uint64_t f = raw[(*i)++];
+ uint64_t g = raw[(*i)++];
+ uint64_t h = raw[(*i)++];
+
+ if (little_endian) *res = a | b << 8 | c << 16 | d << 24 | e << 32 | f << 40 | g << 48 | h << 56;
+ else *res = a << 56 | b << 48 | c << 40 | d << 32 | e << 24 | f << 16 | g << 8 | h;
+
+ return true;
+}
+static bool fmt_read_int64(const uint8_t *raw, size_t *i, size_t size, int64_t *res, bool little_endian) {
+ return fmt_read_uint64(raw, i, size, (uint64_t*)res, little_endian);
+}
+
+static bool fmt_read_float32(const uint8_t *raw, size_t *i, size_t size, float *res, bool little_endian) {
+ if ((*i) + 4 > size || *i < 0) return false;
+
+ uint8_t a = raw[(*i)++];
+ uint8_t b = raw[(*i)++];
+ uint8_t c = raw[(*i)++];
+ uint8_t d = raw[(*i)++];
+
+ // TODO: is this portable enough?
+ if (little_endian) *res = *(float*)(uint8_t[]) { a, b, c, d };
+ else *res = *(float*)(uint8_t[]) { d, c, b, a };
+
+ return true;
+}
+static bool fmt_read_float64(const uint8_t *raw, size_t *i, size_t size, double *res, bool little_endian) {
+ if ((*i) + 8 > size || *i < 0) return false;
+
+ uint8_t a = raw[(*i)++];
+ uint8_t b = raw[(*i)++];
+ uint8_t c = raw[(*i)++];
+ uint8_t d = raw[(*i)++];
+ uint8_t e = raw[(*i)++];
+ uint8_t f = raw[(*i)++];
+ uint8_t g = raw[(*i)++];
+ uint8_t h = raw[(*i)++];
+
+ // TODO: is this portable enough?
+ if (little_endian) *res = *(float*)(uint8_t[]) { a, b, c, d, e, f, g, h };
+ else *res = *(float*)(uint8_t[]) { h, g, f, e, d, c, b, a };
+
+ return true;
+}
+
+static bool fmt_read_string(lua_State *ctx, fmt_segment_t segment, const uint8_t *raw, size_t *i, size_t size, bool little_endian) {
+ uint64_t len;
+
+ switch (segment.type) {
+ case FMT_STR8: {
+ uint8_t len8;
+ if (!fmt_read_uint8(raw, i, size, &len8)) return false;
+ break;
+ }
+ case FMT_STR16: {
+ uint16_t len16;
+ if (!fmt_read_uint16(raw, i, size, &len16, little_endian)) return false;
+ len = len16;
+ break;
+ }
+ case FMT_STR32: {
+ uint32_t len32;
+ if (!fmt_read_uint32(raw, i, size, &len32, little_endian)) return false;
+ len = len32;
+ break;
+ }
+ case FMT_STR64:
+ if (!fmt_read_uint64(raw, i, size, &len, little_endian)) return false;
+ break;
+ case FMT_STR_FIXED:
+ len = segment.size;
+ break;
+ case FMT_STR_ZERO:
+ len = strnlen((const char*)(raw + *i), size - *i);
+ if (len >= size - *i) return false;
+ break;
+ default:
+ return false;
+ }
+
+ fprintf(stderr, "%lu %lu", len, *i);
+ if ((*i) + len > size) return false;
+ if (*i < 0) return false;
+
+ char data[len];
+ memcpy(data, raw + (*i), len);
+
+ lua_pushlstring(ctx, data, len);
+ return true;
+}
+
+
+static void fmt_write_uint8(write_buff_t *buff, uint8_t val) {
+ write_buff_append(buff, (char[]){ val }, 1);
+}
+static void fmt_write_int8(write_buff_t *buff, int8_t val) {
+ return fmt_write_uint8(buff, val);
+}
+
+static void fmt_write_uint16(write_buff_t *buff, uint16_t val, bool little_endian) {
+ uint8_t a = val & 0xFF;
+ uint8_t b = val >> 8 & 0xFF;
+
+ if (little_endian) write_buff_append(buff, (char[]){ a, b }, 2);
+ else write_buff_append(buff, (char[]){ b, a }, 2);
+}
+static void fmt_write_int16(write_buff_t *buff, uint16_t val, bool little_endian) {
+ fmt_write_uint16(buff, val, little_endian);
+}
+
+static void fmt_write_uint32(write_buff_t *buff, uint32_t val, bool little_endian) {
+ uint8_t a = val & 0xFF;
+ uint8_t b = val >> 8 & 0xFF;
+ uint8_t c = val >> 16 & 0xFF;
+ uint8_t d = val >> 24 & 0xFF;
+
+ if (little_endian) write_buff_append(buff, (char[]){ a, b, c, d }, 4);
+ else write_buff_append(buff, (char[]){ d, c, b, a }, 4);
+}
+static void fmt_write_int32(write_buff_t *buff, uint32_t val, bool little_endian) {
+ fmt_write_uint32(buff, val, little_endian);
+}
+
+static void fmt_write_uint64(write_buff_t *buff, uint64_t val, bool little_endian) {
+ uint8_t a = val & 0xFF;
+ uint8_t b = val >> 8 & 0xFF;
+ uint8_t c = val >> 16 & 0xFF;
+ uint8_t d = val >> 24 & 0xFF;
+ uint8_t e = val >> 32 & 0xFF;
+ uint8_t f = val >> 40 & 0xFF;
+ uint8_t g = val >> 48 & 0xFF;
+ uint8_t h = val >> 56 & 0xFF;
+
+ if (little_endian) write_buff_append(buff, (char[]){ a, b, c, d, e, f, g, h }, 8);
+ else write_buff_append(buff, (char[]){ h, g, f, e, d, c, b, a }, 8);
+}
+static void fmt_write_int64(write_buff_t *buff, uint64_t val, bool little_endian) {
+ fmt_write_uint64(buff, val, little_endian);
+}
+
+static void fmt_write_float32(write_buff_t *buff, float val, bool little_endian) {
+ uint32_t ival = *(uint32_t*)&val;
+
+ uint8_t a = ival & 0xFF;
+ uint8_t b = ival >> 8 & 0xFF;
+ uint8_t c = ival >> 16 & 0xFF;
+ uint8_t d = ival >> 24 & 0xFF;
+
+ if (little_endian) write_buff_append(buff, (char[]){ a, b, c, d }, 4);
+ else write_buff_append(buff, (char[]){ d, c, b, a }, 4);
+}
+static void fmt_write_float64(write_buff_t *buff, double val, bool little_endian) {
+ uint64_t ival = *(uint64_t*)&val;
+
+ uint8_t a = ival & 0xFF;
+ uint8_t b = ival >> 8 & 0xFF;
+ uint8_t c = ival >> 16 & 0xFF;
+ uint8_t d = ival >> 24 & 0xFF;
+ uint8_t e = ival >> 32 & 0xFF;
+ uint8_t f = ival >> 40 & 0xFF;
+ uint8_t g = ival >> 48 & 0xFF;
+ uint8_t h = ival >> 56 & 0xFF;
+
+ if (little_endian) write_buff_append(buff, (char[]){ a, b, c, d, e, f, g, h }, 8);
+ else write_buff_append(buff, (char[]){ h, g, f, e, d, c, b, a }, 8);
+}
+
+static bool fmt_write_string(write_buff_t *buff, fmt_segment_t segment, const char *str, size_t len, bool little_endian) {
+ switch (segment.type) {
+ case FMT_STR8:
+ if (len > 256) return false;
+ fmt_write_uint8(buff, (uint8_t)len);
+ write_buff_append(buff, str, len);
+ return true;
+ case FMT_STR16:
+ if (len > 0x10000) return false;
+ fmt_write_uint16(buff, (uint16_t)len, little_endian);
+ write_buff_append(buff, str, len);
+ return true;
+ case FMT_STR32:
+ if (len > 0x100000000) return false;
+ fmt_write_uint32(buff, (uint32_t)len, little_endian);
+ write_buff_append(buff, str, len);
+ return true;
+ case FMT_STR64:
+ fmt_write_uint64(buff, len, little_endian);
+ write_buff_append(buff, str, len);
+ return true;
+ case FMT_STR_FIXED:
+ if (len > segment.size) return false;
+ write_buff_append(buff, str, len);
+ write_buff_append(buff, NULL, segment.size - len);
+ return true;
+ case FMT_STR_ZERO:
+ if (strlen(str) != len) return false;
+ write_buff_append(buff, str, len);
+ write_buff_append(buff, (char[]) { 0x00 }, 1);
+ return true;
+ default:
+ return false;
+ }
+}
+
+
+static int lib_fmt_pack(lua_State *ctx) {
+ fmt_t *fmt = luaL_checkudata(ctx, 1, FMT_UDATA_NAME);
+
+ size_t arg_i = 2;
+ write_buff_t buff = { .arr = NULL, .n = 0, .cap = 0 };
+
+ for (size_t i = 0; i < fmt->segments_n; i++) {
+ fmt_segment_t segment = fmt->segments[i];
+
+ size_t n_aligned = -((-buff.n / segment.align) * segment.align);
+ size_t align_padding = n_aligned - buff.n;
+ write_buff_append(&buff, NULL, align_padding);
+
+ switch (segment.type) {
+ case FMT_INT8:
+ fmt_write_int8(&buff, luaL_checkinteger(ctx, arg_i++));
+ break;
+ case FMT_UINT8:
+ fmt_write_uint8(&buff, luaL_checkinteger(ctx, arg_i++));
+ break;
+ case FMT_INT16:
+ fmt_write_int16(&buff, luaL_checkinteger(ctx, arg_i++), fmt->little_endian);
+ break;
+ case FMT_UINT16:
+ fmt_write_uint16(&buff, luaL_checkinteger(ctx, arg_i++), fmt->little_endian);
+ break;
+ case FMT_INT32:
+ fmt_write_int32(&buff, luaL_checkinteger(ctx, arg_i++), fmt->little_endian);
+ break;
+ case FMT_UINT32:
+ fmt_write_uint32(&buff, luaL_checkinteger(ctx, arg_i++), fmt->little_endian);
+ break;
+ case FMT_INT64:
+ fmt_write_int64(&buff, luaL_checkinteger(ctx, arg_i++), fmt->little_endian);
+ break;
+ case FMT_UINT64:
+ fmt_write_uint64(&buff, luaL_checkinteger(ctx, arg_i++), fmt->little_endian);
+ break;
+
+ case FMT_FLOAT32:
+ fmt_write_float32(&buff, (float)luaL_checknumber(ctx, arg_i++), fmt->little_endian);
+ break;
+ case FMT_FLOAT64:
+ fmt_write_float64(&buff, (double)luaL_checknumber(ctx, arg_i++), fmt->little_endian);
+ break;
+
+ case FMT_STR_FIXED:
+ case FMT_STR8:
+ case FMT_STR16:
+ case FMT_STR32:
+ case FMT_STR64:
+ case FMT_STR_ZERO: {
+ size_t size;
+ const char *str = luaL_checklstring(ctx, arg_i++, &size);
+ if (!fmt_write_string(&buff, segment, str, size, fmt->little_endian)) {
+ luaL_error(ctx, "invalid string at %d", arg_i);
+ }
+ break;
+ }
+
+ case FMT_PADDING:
+ // TODO: might need to remove this later...
+ write_buff_append(&buff, NULL, segment.size);
+ }
+ }
+
+ write_buff_fit(&buff);
+ lua_pushlstring(ctx, buff.arr, buff.n);
+ free(buff.arr);
+
+ return 1;
+}
+
+static int lib_fmt_unpack(lua_State *ctx) {
+ fmt_t *fmt = luaL_checkudata(ctx, 1, FMT_UDATA_NAME);
+
+ size_t raw_size;
+ const uint8_t *raw = (const uint8_t*)luaL_checklstring(ctx, 2, &raw_size);
+
+ size_t read_i = 0;
+ size_t res_n = 0;
+ if (lua_isinteger(ctx, 3)) {
+ read_i = lua_tointeger(ctx, 3) - 1;
+ }
+
+ write_buff_t buff = { .arr = NULL, .n = 0, .cap = 0 };
+
+ for (size_t i = 0; i < fmt->segments_n; i++) {
+ fmt_segment_t segment = fmt->segments[i];
+
+ size_t n_aligned = -((-buff.n / segment.align) * segment.align);
+ size_t align_padding = n_aligned - buff.n;
+ read_i += align_padding;
+
+ switch (segment.type) {
+ case FMT_INT8: {
+ int8_t res;
+ if (!fmt_read_int8(raw, &read_i, raw_size, &res)) luaL_error(ctx, "couldn't read int8");
+ lua_pushinteger(ctx, res);
+ res_n++;
+ break;
+ }
+ case FMT_UINT8: {
+ uint8_t res;
+ if (!fmt_read_uint8(raw, &read_i, raw_size, &res)) luaL_error(ctx, "couldn't read uint8");
+ lua_pushinteger(ctx, res);
+ res_n++;
+ break;
+ }
+ case FMT_INT16: {
+ int16_t res;
+ if (!fmt_read_int16(raw, &read_i, raw_size, &res, fmt->little_endian)) luaL_error(ctx, "couldn't read int16");
+ lua_pushinteger(ctx, res);
+ res_n++;
+ break;
+ }
+ case FMT_UINT16: {
+ uint16_t res;
+ if (!fmt_read_uint16(raw, &read_i, raw_size, &res, fmt->little_endian)) luaL_error(ctx, "couldn't read uint16");
+ lua_pushinteger(ctx, res);
+ res_n++;
+ break;
+ }
+ case FMT_INT32: {
+ int32_t res;
+ if (!fmt_read_int32(raw, &read_i, raw_size, &res, fmt->little_endian)) luaL_error(ctx, "couldn't read int32");
+ lua_pushinteger(ctx, res);
+ res_n++;
+ break;
+ }
+ case FMT_UINT32: {
+ uint32_t res;
+ if (!fmt_read_uint32(raw, &read_i, raw_size, &res, fmt->little_endian)) luaL_error(ctx, "couldn't read uint32");
+ lua_pushinteger(ctx, res);
+ res_n++;
+ break;
+ }
+ case FMT_INT64: {
+ int64_t res;
+ if (!fmt_read_int64(raw, &read_i, raw_size, &res, fmt->little_endian)) luaL_error(ctx, "couldn't read int64");
+ lua_pushinteger(ctx, res);
+ res_n++;
+ break;
+ }
+ case FMT_UINT64: {
+ uint64_t res;
+ if (!fmt_read_uint64(raw, &read_i, raw_size, &res, fmt->little_endian)) luaL_error(ctx, "couldn't read uint64");
+ lua_pushinteger(ctx, res);
+ res_n++;
+ break;
+ }
+
+ case FMT_FLOAT32: {
+ float res;
+ if (!fmt_read_float32(raw, &read_i, raw_size, &res, fmt->little_endian)) luaL_error(ctx, "couldn't read float32");
+ lua_pushinteger(ctx, res);
+ res_n++;
+ break;
+ }
+ case FMT_FLOAT64: {
+ double res;
+ if (!fmt_read_float64(raw, &read_i, raw_size, &res, fmt->little_endian)) luaL_error(ctx, "couldn't read float64");
+ lua_pushinteger(ctx, res);
+ res_n++;
+ break;
+ }
+
+ case FMT_STR_FIXED:
+ case FMT_STR8:
+ case FMT_STR16:
+ case FMT_STR32:
+ case FMT_STR64:
+ case FMT_STR_ZERO: {
+ if (!fmt_read_string(ctx, segment, raw, &read_i, raw_size, fmt->little_endian)) luaL_error(ctx, "couldn't read string");
+ res_n++;
+ break;
+ }
+
+ case FMT_PADDING:
+ read_i += segment.size;
+ break;
+ }
+ }
+
+ lua_pushinteger(ctx, read_i + 1);
+ return res_n + 1;
+}
+
+
+static int lib_fmt_new(lua_State *ctx) {
+ size_t raw_size;
+ const char *raw = luaL_checklstring(ctx, 1, &raw_size);
+ fmt_code_t code = fmt_parse_fmt(ctx, raw, raw_size);
+
+ if (code != FMT_OK) {
+ switch (code) {
+ case FMT_BAD_ARGS: return luaL_error(ctx, "illegal C arguments");
+ case FMT_INCOMPLETE_OP: return luaL_error(ctx, "incomplete operand in format string");
+ case FMT_BAD_OP_ARG: return luaL_error(ctx, "bad operand argument in format string");
+ case FMT_BAD_OP: return luaL_error(ctx, "bad operand in format string");
+ default: return luaL_error(ctx, "unknown error while parsing format string");
+ }
+ }
+
+ return 1;
+}
+
+
+static void fmt_init_meta(lua_State *ctx) {
+ luaL_newmetatable(ctx, FMT_UDATA_NAME);
+
+ luaL_newlib(ctx, ((luaL_Reg[]) {
+ { "pack", lib_fmt_pack },
+ { "unpack", lib_fmt_unpack },
+ { NULL, NULL }
+ }));
+ lua_setfield(ctx, -2, "__index");
+
+ lua_pushboolean(ctx, false);
+ lua_setfield(ctx, -2, "__meta");
+
+ lua_pop(ctx, 1);
+}
+
+int fmt_open_lib(lua_State *ctx) {
+ fmt_init_meta(ctx);
+
+ luaL_newlib(ctx, ((luaL_Reg[]) {
+ { "new", lib_fmt_new },
+ { "pack", lib_fmt_pack },
+ { "unpack", lib_fmt_unpack },
+ { NULL, NULL },
+ }));
+
+ return 1;
+}
diff --git a/lib/http.c b/lib/http.c
new file mode 100644
index 0000000..9228391
--- /dev/null
+++ b/lib/http.c
@@ -0,0 +1,101 @@
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "lib.h"
+
+typedef struct {
+ char **segments;
+ size_t *sizes;
+ size_t n, cap;
+} http_body_buff_t;
+
+static size_t body_writer(char *ptr, size_t _, size_t size, void *pbuff) {
+ http_body_buff_t *buff = pbuff;
+
+ size_t new_cap = buff->cap;
+ while (buff->n >= new_cap) {
+ new_cap *= 2;
+ }
+
+ if (new_cap > buff->cap) {
+ buff->segments = realloc(buff->segments, sizeof(*buff->segments) * new_cap);
+ buff->sizes = realloc(buff->sizes, sizeof(*buff->sizes) * new_cap);
+ buff->cap = new_cap;
+ }
+
+ buff->segments[buff->n] = malloc(size);
+ memcpy(buff->segments[buff->n], ptr, size);
+ buff->sizes[buff->n] = size;
+ buff->n++;
+
+
+ return size;
+}
+
+static char *body_compress(http_body_buff_t buff, size_t *psize) {
+ size_t size = 0;
+
+ for (size_t i = 0; i < buff.n; i++) {
+ size += buff.sizes[i];
+ }
+
+ char *res = malloc(size + 1);
+ res[size] = 0;
+
+ size_t offset = 0;
+
+ for (size_t i = 0; i < buff.n; i++) {
+ memcpy(res + offset, buff.segments[i], buff.sizes[i]);
+ offset += buff.sizes[i];
+ free(buff.segments[i]);
+ }
+
+ free(buff.segments);
+ free(buff.sizes);
+
+ *psize = size;
+ return res;
+}
+
+static int lib_http_get(lua_State *ctx) {
+ const char *url = luaL_checkstring(ctx, 1);
+
+ http_body_buff_t buff = {
+ .segments = malloc(sizeof *buff.segments * 16),
+ .sizes = malloc(sizeof *buff.sizes * 16),
+ .n = 0,
+ .cap = 16,
+ };
+
+ CURL *curl = curl_easy_init();
+ curl_easy_setopt(curl, CURLOPT_URL, url);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, body_writer);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buff);
+
+ CURLcode code = curl_easy_perform(curl);
+ curl_easy_cleanup(curl);
+
+ if (code != CURLE_OK) {
+ return luaL_error(ctx, "HTTP GET failed: %s", curl_easy_strerror(code));
+ }
+
+ size_t size;
+ char *body = body_compress(buff, &size);
+ lua_pushlstring(ctx, body, size);
+ free(body);
+
+ return 1;
+}
+
+int http_open_lib(lua_State *ctx) {
+ luaL_newlib(ctx, ((luaL_Reg[]) {
+ { "get", lib_http_get },
+ { NULL, NULL },
+ }));
+
+ return 1;
+}
diff --git a/lib/lib.h b/lib/lib.h
new file mode 100644
index 0000000..9c5a6e3
--- /dev/null
+++ b/lib/lib.h
@@ -0,0 +1,5 @@
+#include
+
+int http_open_lib(lua_State *ctx);
+int fmt_open_lib(lua_State *ctx);
+int zlib_open_lib(lua_State *ctx);
diff --git a/lib/main.c b/lib/main.c
new file mode 100644
index 0000000..5f459ef
--- /dev/null
+++ b/lib/main.c
@@ -0,0 +1,126 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "lib.h"
+
+#ifndef BYTECODE
+ static char entry_bytecode[] = {
+ 0x1b, 0x4c, 0x75, 0x61, 0x54, 0x00, 0x19, 0x93,
+ 0x0d, 0x0a, 0x1a, 0x0a, 0x04, 0x08, 0x08, 0x78,
+ 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x28, 0x77, 0x40, 0x01,
+ 0x80, 0x80, 0x80, 0x00, 0x01, 0x02, 0x85, 0x51,
+ 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x83,
+ 0x80, 0x00, 0x00, 0x44, 0x00, 0x02, 0x01, 0x46,
+ 0x00, 0x01, 0x01, 0x82, 0x04, 0x86, 0x65, 0x72,
+ 0x72, 0x6f, 0x72, 0x04, 0xa4, 0x70, 0x6c, 0x65,
+ 0x61, 0x73, 0x65, 0x2c, 0x20, 0x63, 0x6f, 0x6d,
+ 0x70, 0x69, 0x6c, 0x65, 0x20, 0x77, 0x69, 0x74,
+ 0x68, 0x20, 0x2d, 0x44, 0x42, 0x59, 0x54, 0x45,
+ 0x43, 0x4f, 0x44, 0x45, 0x3d, 0x2e, 0x2e, 0x2e,
+ 0x81, 0x01, 0x00, 0x00, 0x80, 0x80, 0x80, 0x80,
+ 0x80,
+ };
+ #define BYTECODE entry_bytecode
+#endif
+
+static char err_bytecode[] = {
+ 0x1b, 0x4c, 0x75, 0x61, 0x54, 0x00, 0x19, 0x93,
+ 0x0d, 0x0a, 0x1a, 0x0a, 0x04, 0x08, 0x08, 0x78,
+ 0x56, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x28, 0x77, 0x40, 0x01,
+ 0x80, 0x80, 0x80, 0x00, 0x01, 0x0c, 0xc4, 0x51,
+ 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x02, 0x81,
+ 0x80, 0x00, 0x80, 0x0b, 0x01, 0x00, 0x00, 0x0e,
+ 0x01, 0x02, 0x01, 0x14, 0x81, 0x02, 0x02, 0x03,
+ 0x82, 0x01, 0x00, 0x8b, 0x02, 0x00, 0x04, 0x00,
+ 0x03, 0x00, 0x00, 0xc4, 0x02, 0x02, 0x02, 0x03,
+ 0x83, 0x02, 0x00, 0x44, 0x01, 0x05, 0x01, 0x0b,
+ 0x01, 0x00, 0x06, 0x0e, 0x01, 0x02, 0x07, 0x80,
+ 0x01, 0x01, 0x00, 0x03, 0x02, 0x04, 0x00, 0x44,
+ 0x01, 0x03, 0x02, 0x3c, 0x81, 0x09, 0x00, 0x38,
+ 0x17, 0x00, 0x80, 0x8e, 0x01, 0x02, 0x0a, 0x0e,
+ 0x02, 0x02, 0x0b, 0x3c, 0x02, 0x0c, 0x00, 0xb8,
+ 0x00, 0x00, 0x80, 0x03, 0x82, 0x06, 0x00, 0x38,
+ 0x05, 0x00, 0x80, 0x94, 0x82, 0x04, 0x0e, 0x83,
+ 0x83, 0x07, 0x00, 0xc4, 0x02, 0x03, 0x02, 0xc2,
+ 0x02, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x80, 0x03,
+ 0x02, 0x08, 0x00, 0xb8, 0x01, 0x00, 0x80, 0x83,
+ 0x82, 0x08, 0x00, 0x00, 0x03, 0x04, 0x00, 0xb5,
+ 0x02, 0x02, 0x00, 0x00, 0x02, 0x05, 0x00, 0x8e,
+ 0x02, 0x02, 0x12, 0xc0, 0x02, 0x7f, 0x00, 0x38,
+ 0x02, 0x00, 0x80, 0x80, 0x02, 0x04, 0x00, 0x03,
+ 0x83, 0x09, 0x00, 0x8e, 0x03, 0x02, 0x12, 0xb5,
+ 0x02, 0x03, 0x00, 0x00, 0x02, 0x05, 0x00, 0xbc,
+ 0x81, 0x09, 0x00, 0xb8, 0x04, 0x00, 0x80, 0x8b,
+ 0x02, 0x00, 0x00, 0x8e, 0x02, 0x05, 0x01, 0x94,
+ 0x82, 0x05, 0x02, 0x83, 0x03, 0x0a, 0x00, 0x00,
+ 0x04, 0x04, 0x00, 0x83, 0x84, 0x0a, 0x00, 0x00,
+ 0x05, 0x03, 0x00, 0x83, 0x85, 0x02, 0x00, 0xc4,
+ 0x02, 0x07, 0x01, 0x38, 0x03, 0x00, 0x80, 0x8b,
+ 0x02, 0x00, 0x00, 0x8e, 0x02, 0x05, 0x01, 0x94,
+ 0x82, 0x05, 0x02, 0x83, 0x03, 0x0a, 0x00, 0x00,
+ 0x04, 0x04, 0x00, 0x83, 0x84, 0x02, 0x00, 0xc4,
+ 0x02, 0x05, 0x01, 0x95, 0x00, 0x01, 0x80, 0xaf,
+ 0x00, 0x80, 0x06, 0xb8, 0xe4, 0xff, 0x7f, 0x46,
+ 0x00, 0x02, 0x01, 0x46, 0x01, 0x01, 0x01, 0x96,
+ 0x04, 0x83, 0x69, 0x6f, 0x04, 0x87, 0x73, 0x74,
+ 0x64, 0x65, 0x72, 0x72, 0x04, 0x86, 0x77, 0x72,
+ 0x69, 0x74, 0x65, 0x04, 0x92, 0x75, 0x6e, 0x68,
+ 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x64, 0x20, 0x65,
+ 0x72, 0x72, 0x6f, 0x72, 0x3a, 0x20, 0x04, 0x89,
+ 0x74, 0x6f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67,
+ 0x04, 0x82, 0x0a, 0x04, 0x86, 0x64, 0x65, 0x62,
+ 0x75, 0x67, 0x04, 0x88, 0x67, 0x65, 0x74, 0x69,
+ 0x6e, 0x66, 0x6f, 0x04, 0x84, 0x53, 0x6e, 0x6c,
+ 0x00, 0x04, 0x85, 0x6e, 0x61, 0x6d, 0x65, 0x04,
+ 0x8a, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x73,
+ 0x72, 0x63, 0x04, 0x84, 0x5b, 0x43, 0x5d, 0x04,
+ 0x87, 0x61, 0x74, 0x20, 0x3c, 0x43, 0x3e, 0x04,
+ 0x85, 0x66, 0x69, 0x6e, 0x64, 0x04, 0x89, 0x25,
+ 0x5b, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x04,
+ 0x8c, 0x61, 0x74, 0x20, 0x3c, 0x73, 0x74, 0x72,
+ 0x69, 0x6e, 0x67, 0x3e, 0x04, 0x84, 0x61, 0x74,
+ 0x20, 0x04, 0x8c, 0x63, 0x75, 0x72, 0x72, 0x65,
+ 0x6e, 0x74, 0x6c, 0x69, 0x6e, 0x65, 0x04, 0x82,
+ 0x3a, 0x04, 0x83, 0x20, 0x20, 0x04, 0x85, 0x20,
+ 0x69, 0x6e, 0x20, 0x81, 0x01, 0x00, 0x00, 0x80,
+ 0x80, 0x80, 0x80, 0x80,
+};
+
+static void load_modules(lua_State *ctx) {
+ luaL_openlibs(ctx);
+ luaL_requiref(ctx, "http", http_open_lib, false);
+ luaL_requiref(ctx, "fmt", fmt_open_lib, false);
+ luaL_requiref(ctx, "zlib", zlib_open_lib, false);
+}
+
+int main(int argc, const char **argv) {
+ lua_State *ctx = luaL_newstate();
+ load_modules(ctx);
+
+ if (luaL_loadbufferx(ctx, err_bytecode, sizeof(err_bytecode), "", "b")) {
+ fprintf(stderr, "error while loading lua bytecode for errfunc: %s", lua_tostring(ctx, -1));
+ return 1;
+ }
+ int errfunc_i = lua_gettop(ctx);
+
+ if (luaL_loadbufferx(ctx, (char*)BYTECODE, sizeof(BYTECODE), "", "b")) {
+ fprintf(stderr, "error while loading lua bytecode for main: %s", lua_tostring(ctx, -1));
+ return 1;
+ }
+
+ for (size_t i = 1; i < argc; i++) {
+ lua_pushstring(ctx, argv[i]);
+ }
+
+ if (lua_pcall(ctx, argc - 1, 0, errfunc_i)) return 1;
+
+ lua_close(ctx);
+ return 0;
+}
diff --git a/lib/zlib.c b/lib/zlib.c
new file mode 100644
index 0000000..8009b7d
--- /dev/null
+++ b/lib/zlib.c
@@ -0,0 +1,136 @@
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "lib.h"
+
+#define DECOMPRESS_META "zlib.decompress.meta"
+
+typedef struct {
+ z_stream stream;
+ size_t processed;
+ uint8_t buff[4096];
+ bool finalized;
+} lua_zlib_stream_t;
+
+static int lib_decompress_iter(lua_State *ctx) {
+ size_t size;
+ uint8_t *data = (uint8_t*)lua_tolstring(ctx, lua_upvalueindex(1), &size);
+
+ lua_zlib_stream_t *stream = lua_touserdata(ctx, lua_upvalueindex(2));
+
+ if (stream->finalized) {
+ lua_pushnil(ctx);
+ return 1;
+ }
+
+ stream->stream.next_in = data + stream->processed;
+ stream->stream.avail_in = size - stream->processed;
+
+ stream->stream.next_out = stream->buff;
+ stream->stream.avail_out = sizeof stream->buff;
+
+ int res = inflate(&stream->stream, Z_NO_FLUSH);
+
+ switch (res) {
+ case Z_STREAM_END:
+ inflateEnd(&stream->stream);
+ stream->finalized = true;
+ case Z_OK: {
+ lua_pushlstring(ctx, (const char*)stream->buff, sizeof stream->buff - stream->stream.avail_out);
+ stream->processed = size - stream->stream.avail_in;
+ return 1;
+ }
+ case Z_BUF_ERROR:
+ inflateEnd(&stream->stream);
+ stream->finalized = true;
+ return luaL_error(ctx, "zlib error: incorrect C parameters passed");
+ case Z_MEM_ERROR:
+ inflateEnd(&stream->stream);
+ stream->finalized = true;
+ return luaL_error(ctx, "zlib error: memory ran out");
+ case Z_STREAM_ERROR:
+ inflateEnd(&stream->stream);
+ stream->finalized = true;
+ return luaL_error(ctx, "zlib error: invalid state");
+ case Z_DATA_ERROR:
+ inflateEnd(&stream->stream);
+ stream->finalized = true;
+ return luaL_error(ctx, "zlib error: %s", stream->stream.msg);
+ default:
+ return luaL_error(ctx, "wtf");
+ }
+}
+static int lib_decompress_free(lua_State *ctx) {
+ lua_zlib_stream_t *stream = lua_touserdata(ctx, 1);
+ if (stream->finalized) return 0;
+
+ inflateEnd(&stream->stream);
+ stream->finalized = true;
+
+ return 0;
+}
+static int lib_decompress_create(lua_State *ctx, int window_size) {
+ // size_t data_size;
+ // uint8_t *data = (uint8_t*)luaL_checklstring(ctx, 1, &data_size);
+
+ lua_pushvalue(ctx, 1);
+
+ lua_zlib_stream_t *stream = lua_newuserdatauv(ctx, sizeof *stream, 0);
+ memset(stream, 0, sizeof *stream);
+
+ luaL_getmetatable(ctx, DECOMPRESS_META);
+ lua_setmetatable(ctx, -2);
+
+
+ // stream->stream.avail_out = sizeof stream->buff;
+ // stream->stream.next_out = stream->buff;
+
+ // stream->stream.avail_in = data_size;
+ // stream->stream.next_in = data;
+
+ if (inflateInit2(&stream->stream, window_size) != Z_OK) {
+ return luaL_error(ctx, "failed to initialize inflate stream");
+ }
+
+ lua_pushcclosure(ctx, lib_decompress_iter, 2);
+ return 1;
+}
+
+
+static int lib_inflate(lua_State *ctx) {
+ return lib_decompress_create(ctx, MAX_WBITS);
+}
+static int lib_inflate_raw(lua_State *ctx) {
+ return lib_decompress_create(ctx, -MAX_WBITS);
+}
+static int lib_gunzip(lua_State *ctx) {
+ return lib_decompress_create(ctx, MAX_WBITS + 16);
+}
+static int lib_decompress(lua_State *ctx) {
+ return lib_decompress_create(ctx, MAX_WBITS + 32);
+}
+
+
+int zlib_open_lib(lua_State *ctx) {
+ luaL_newmetatable(ctx, DECOMPRESS_META);
+ luaL_setfuncs(ctx, (luaL_Reg[]) {
+ { "__gc", lib_decompress_free },
+ { NULL, NULL },
+ }, 0);
+
+ lua_pop(ctx, 2);
+
+ luaL_newlib(ctx, ((luaL_Reg[]) {
+ { "inflate", lib_inflate },
+ { "inflate_raw", lib_inflate_raw },
+ { "gunzip", lib_gunzip },
+ { "decompress", lib_decompress },
+ { NULL, NULL },
+ }));
+
+ return 1;
+}
diff --git a/mod/fmt.d.lua b/mod/fmt.d.lua
new file mode 100644
index 0000000..1ef551b
--- /dev/null
+++ b/mod/fmt.d.lua
@@ -0,0 +1,24 @@
+--- @meta fmt
+
+--- @class fmt
+local fmt_proto = {};
+
+--- @param ... any
+--- @return string
+function fmt_proto:pack(...) end
+
+--- @param str string
+--- @param i? integer
+--- @return ...
+function fmt_proto:unpack(str, i) end
+
+local fmt = {};
+
+--- @param path string
+--- @return fmt fmt
+function fmt.new(path) end
+
+fmt.pack = fmt_proto.pack;
+fmt.unpack = fmt_proto.unpack;
+
+return fmt;
diff --git a/mod/http.d.lua b/mod/http.d.lua
new file mode 100644
index 0000000..a37f895
--- /dev/null
+++ b/mod/http.d.lua
@@ -0,0 +1,9 @@
+--- @meta http
+
+local http = {};
+
+--- @param path string
+--- @return string body
+function http.get(path) end
+
+return http;
diff --git a/mod/zlib.d.lua b/mod/zlib.d.lua
new file mode 100644
index 0000000..e3825fe
--- /dev/null
+++ b/mod/zlib.d.lua
@@ -0,0 +1,17 @@
+--- @meta zlib
+
+local zlib = {};
+
+--- @param data string
+--- @return fun(): string?
+function zlib.inflate_raw(data) end
+
+--- @param data string
+--- @return fun(): string?
+function zlib.gunzip(data) end
+
+--- @param data string
+--- @return fun(): string?
+function zlib.inflate(data) end
+
+return zlib;
diff --git a/src/cli/dump.lua b/src/cli/dump.lua
new file mode 100644
index 0000000..915c7e7
--- /dev/null
+++ b/src/cli/dump.lua
@@ -0,0 +1,41 @@
+local ostree = require "ostree";
+-- TODO: make this an option
+local repo_url = "https://dl.flathub.org/repo";
+
+return function (name, out)
+ local repo = ostree.from_http(repo_url);
+ local ref = repo:read_ref(name);
+ local commit = repo:read_commit(ref);
+
+ local function dump_file(path, file)
+ print("Dumping " .. path .. "...");
+
+ local file_obj = repo:read_file(file);
+
+ if file_obj.type == "file" then
+ local f = assert(io.open(out .. "/" .. path, "w"));
+ for seg in file_obj.get_content() do
+ assert(f:write(seg));
+ end
+ f:close();
+ else
+ os.execute(("ln -s %q %q"):format(file_obj.target, out .. "/" .. path));
+ end
+ end
+ local function dump_dir(path, dir)
+ print("Dumping " .. path .. "...");
+ os.execute(("mkdir %q"):format(out .. "/" .. path));
+
+ local dir_obj = repo:read_dir(dir);
+
+ for subname, subdir in pairs(dir_obj.dirs) do
+ dump_dir(path .. "/" .. subname, subdir);
+ end
+
+ for subname, subfile in pairs(dir_obj.files) do
+ dump_file(path .. "/" .. subname, subfile);
+ end
+ end
+
+ dump_dir("/", commit.root);
+end
diff --git a/src/cli/help.lua b/src/cli/help.lua
new file mode 100644
index 0000000..e829d0a
--- /dev/null
+++ b/src/cli/help.lua
@@ -0,0 +1,14 @@
+return function (action, ...)
+ print "TopchetoEU's slimpack:";
+ print "Minimalistic flatpak client, used to run flatpaks";
+ print "with less isolation (to hell with security)";
+ print "";
+ print "How to use:";
+ print "The CLI accepts one 'action' argument, and the rest are specific to the action.";
+ print "The following actions are currently supported:";
+ print "query list - lists all refs in the current repo (be warned, this will produce over 20K lines of output)";
+ print "query search - searches the given term in the current repo";
+ print "dump [ [out] - dumps the raw contents of the given package in the given dirname (defaults to 'out')";
+ print "help (or --help/-h/-help) - shows this message";
+ os.exit();
+end
diff --git a/src/cli/query.lua b/src/cli/query.lua
new file mode 100644
index 0000000..c962d81
--- /dev/null
+++ b/src/cli/query.lua
@@ -0,0 +1,28 @@
+local ostree = require "ostree";
+local repo_url = "https://dl.flathub.org/repo";
+
+local actions = {};
+function actions.list()
+ local repo = ostree.from_http(repo_url);
+ local index = repo:read_summary_index();
+ local summary = repo:read_subsummary(index.refs.x86_64.checksum);
+
+ for ref in pairs(summary.refs) do
+ print(ref);
+ end
+end
+function actions.search(term)
+ local repo = ostree.from_http(repo_url);
+ local index = repo:read_summary_index();
+ local summary = repo:read_subsummary(index.refs.x86_64.checksum);
+
+ for ref in pairs(summary.refs) do
+ if ref:lower():match(term:lower(), 1, true) then
+ print(ref);
+ end
+ end
+end
+
+return function (action, ...)
+ actions[action](...);
+end
diff --git a/src/errfunc.lua b/src/errfunc.lua
new file mode 100644
index 0000000..c265773
--- /dev/null
+++ b/src/errfunc.lua
@@ -0,0 +1,34 @@
+local err = ...;
+local i = 2;
+
+io.stderr:write("unhandled error: ", tostring(err), "\n");
+
+while true do
+ local info = debug.getinfo(i, "Snl");
+ if info == nil then break end
+
+ local name = info.name;
+ local location = info.short_src;
+
+ if location == "[C]" then
+ location = "at ";
+ elseif location:find "%[string" then
+ location = "at ";
+ else
+ location = "at " .. location;
+ end
+
+ if info.currentline > 0 then
+ location = location .. ":" .. info.currentline;
+ end
+
+ if name ~= nil then
+ io.stderr:write(" ", location, " in ", name, "\n");
+ else
+ io.stderr:write(" ", location, "\n");
+ end
+
+ i = i + 1;
+end
+
+return err;
diff --git a/src/formats/gvariant.lua b/src/formats/gvariant.lua
new file mode 100644
index 0000000..5b09616
--- /dev/null
+++ b/src/formats/gvariant.lua
@@ -0,0 +1,401 @@
+-- TODO: remove string.unpack
+
+local fmt = require "fmt";
+
+local uint8_le_fmt = fmt.new "! I1", 1, 1);
+parsers.uint16 = fmt_to_parser(fmt.new ">! I2", 2, 2);
+parsers.uint32 = fmt_to_parser(fmt.new ">! I4", 4, 4);
+parsers.uint64 = fmt_to_parser(fmt.new ">! I8", 8, 8);
+
+parsers.int8 = fmt_to_parser(fmt.new ">! i1", 1, 1);
+parsers.int16 = fmt_to_parser(fmt.new ">! i2", 2, 2);
+parsers.int32 = fmt_to_parser(fmt.new ">! i4", 4, 4);
+parsers.int64 = fmt_to_parser(fmt.new ">! i8", 8, 8);
+
+parsers.float32 = fmt_to_parser(fmt.new ">! f", 4, 4);
+parsers.float64 = fmt_to_parser(fmt.new ">! d", 8, 8);
+
+function parsers.string()
+ return {
+ fixed = nil, align = 1,
+ read = function(buf, s, e)
+ return buf:sub(s, e - 1);
+ end,
+ };
+end
+function parsers.ref()
+ return {
+ fixed = nil, align = 1,
+ read = function(buf, s, e)
+ return to_encoded(buf:sub(s, e));
+ end,
+ };
+end
+function parsers.buffer()
+ return {
+ fixed = nil, align = 1,
+ read = function(buf, s, e)
+ return buf:sub(s, e);
+ end,
+ };
+end
+
+function parsers.bool()
+ return {
+ fixed = 1, align = 1,
+ read = function(buf, s, e)
+ return buf:sub(s, e) == "\1";
+ end,
+ };
+end
+
+function parsers.maybe(val_parser)
+ if val_parser.fixed then
+ return {
+ fixed = nil, align = val_parser.align,
+ read = function(buf, s, e)
+ if s > e then
+ return nil;
+ else
+ return val_parser.read(buf, alignof(s, val_parser.align), e);
+ end
+ end
+ };
+ else
+ return {
+ fixed = nil, align = val_parser.align,
+ read = function(buf, s, e)
+ if s > e then
+ return nil;
+ else
+ return val_parser.read(buf, alignof(s, val_parser.align), e - 1);
+ end
+ end
+ };
+ end
+end
+
+function parsers.array(el_parser, as_iterable)
+ return {
+ fixed = nil, align = el_parser.align,
+ read = function(buf, s, e)
+ if s > e then
+ if as_iterable then
+ return function () return nil end
+ else
+ return {};
+ end
+ end
+
+ s = alignof(s, el_parser.align);
+
+ local read_offset, offset_size = offset_reader(s, e);
+ local n = ((e - s + 1) - read_offset(buf, 1)) / offset_size;
+ local curr_s = s;
+
+ -- print(indent, "ARR\t", s, e, n, read_offset(buf, 1));
+ -- indent = indent .. "\t";
+
+ assert(not (n % 1 > 0), "offset calculation error");
+ n = math.floor(n);
+
+ if as_iterable then
+ local i = 0;
+
+ return function ()
+ if i >= n then
+ -- indent = indent:sub(1, -2);
+ return nil;
+ end
+ i = i + 1;
+
+ local curr_e = s + read_offset(buf, n - i + 1) - 1;
+ curr_s = alignof(curr_s, el_parser.align);
+
+ -- print(indent, "ARR ELEMENT", i, curr_s, curr_e);
+ local res = el_parser.read(buf, curr_s, curr_e);
+
+ curr_s = curr_e + 1;
+ return res;
+ end
+ else
+ local res = {};
+ for i = 1, n do
+ local curr_e = s + read_offset(buf, n - i + 1) - 1;
+ curr_s = alignof(curr_s, el_parser.align);
+
+ -- print(indent, "ARR ELEMENT", i, curr_s, curr_e);
+ res[i] = el_parser.read(buf, curr_s, curr_e);
+
+ curr_s = curr_e + 1;
+ end
+
+ -- indent = indent:sub(1, -2);
+ return res;
+ end
+ end
+ };
+end
+
+function parsers.map(key, val)
+ local arr_parser = parsers.array(parsers.tuple(key, val), true);
+
+ return {
+ fixed = nil, align = arr_parser.align,
+ read = function (buf, s, e)
+ local res = {};
+
+ for el in arr_parser.read(buf, s, e) do
+ res[el[1]] = el[2];
+ end
+
+ return res;
+ end
+ }
+end
+
+function parsers.tuple(...)
+ local descriptors = { ... };
+
+ local align = 1;
+ --- @type integer | nil
+ local fixed = 0;
+
+ for i = 1, select("#", ...) do
+ assert(select(i, ...) ~= nil, "don't pass nil arguments!");
+
+ local parser = descriptors[i];
+
+ if align < parser.align then
+ align = parser.align;
+ end
+
+ if parser.fixed == nil then
+ fixed = nil;
+ elseif fixed ~= nil then
+ fixed = alignof(fixed, parser.align) + parser.fixed;
+ end
+ end
+
+ return {
+ fixed = fixed, align = align,
+ read = function (buff, s, e)
+ if s > e then return {} end
+
+ s = alignof(s, align);
+ -- print(indent, "TUPLE\t", s, e);
+ -- indent = indent .. "\t";
+
+ local read_offset, offset_size = offset_reader(s, e);
+ local res = {};
+ local curr_s = s;
+
+ local offset_i = 0;
+
+ for i = 1, #descriptors do
+ curr_s = alignof(curr_s, descriptors[i].align);
+ local curr_e;
+
+ if descriptors[i].fixed then
+ curr_e = curr_s + descriptors[i].fixed - 1;
+ -- print(indent, "TUPLE FIXEL", i, curr_s, curr_e);
+ elseif i == #descriptors then
+ curr_e = e - offset_size * offset_i;
+ -- print(indent, "TUPLE LASTEL", i, curr_s, curr_e);
+ else
+ offset_i = offset_i + 1;
+ curr_e = s + read_offset(buff, offset_i) - 1;
+ -- print(indent, "TUPLE DYNEL", i, curr_s, curr_e);
+ end
+
+ assert(curr_e - curr_s + 1 >= 0, "negative length segment");
+
+ res[i] = descriptors[i].read(buff, curr_s, curr_e);
+ curr_s = curr_e + 1;
+ end
+
+ -- indent = indent:sub(1, -2);
+ return res;
+ end
+ };
+end
+
+function parsers.variant()
+ return {
+ fixed = nil, align = 8,
+ read = function (buff, s, e)
+ local format = buff:sub(s, e):match "\0([^\0]+)$";
+ local parser = construct_reader(format);
+ -- print(indent, "VARIANT", format);
+ return parser.read(buff, alignof(s, parser.align), e - #format - 1);
+ end
+ };
+end
+
+return function (format)
+ local readers = {};
+
+ while #format > 0 do
+ local i;
+ readers[#readers + 1], i = construct_reader(format);
+ format = format:sub(i);
+ end
+
+ if #readers == 0 then
+ return function () end
+ elseif #readers == 1 then
+ local reader = readers[1];
+
+ return function (buff, s, e)
+ return reader.read(buff, s or 1, e or #buff);
+ end
+ else
+ local tuple_parser = parsers.tuple(table.unpack(readers));
+
+ return function (buff, s, e)
+ return table.unpack(tuple_parser.read(buff, s or 1, e or #buff));
+ end
+ end
+end
diff --git a/src/formats/ini.lua b/src/formats/ini.lua
new file mode 100644
index 0000000..15b8880
--- /dev/null
+++ b/src/formats/ini.lua
@@ -0,0 +1,59 @@
+local function parse_ini(raw, glob_group)
+ local lines = {};
+
+ for line in raw:gmatch "[^\n]+" do
+ lines[#lines + 1] = line:match "^%s+(.+)%s+$";
+ end
+
+ local groups = {};
+ local curr_group;
+
+ if glob_group then
+ curr_group = {};
+ groups[glob_group] = curr_group;
+ end
+
+ local line_i = 0;
+
+ for line in raw:gmatch "[^\n]+" do
+ line_i = line_i + 1;
+ line = line:match "^%s*(.-)%s*$";
+
+ if line ~= "" then
+ local group = line.match "^%[%s*(.-)%s*%]$";
+
+ if group ~= nil then
+ curr_group = {};
+ groups[group] = curr_group;
+ elseif curr_group == nil then
+ error("line " .. line_i .. ": Unexpected global key");
+ else
+ local key, value = line.match "^%s*(.-)%s*=%s*(.-)%s*$";
+ if key == nil then
+ error("line " .. line_i .. ": Unexpected ini syntax");
+ end
+
+ curr_group[key] = value;
+ end
+ end
+ end
+
+ return groups;
+end
+
+local function parse_ini_list(raw)
+ local res = {};
+
+ for el in raw:gmatch "%s*([^;]-)%s*" do
+ if el ~= "" then
+ res[#res + 1] = el;
+ end
+ end
+
+ return res;
+end
+
+return {
+ parse = parse_ini,
+ list = parse_ini_list,
+};
diff --git a/src/formats/xml.lua b/src/formats/xml.lua
new file mode 100644
index 0000000..bf80ac7
--- /dev/null
+++ b/src/formats/xml.lua
@@ -0,0 +1,251 @@
+--- @alias xml_node_raw { tag: string, attribs: { [string]: string }, [integer]: xml_element }
+--- @alias xml_element string | xml_node
+
+--- @class xml_node
+--- @field tag string
+--- @field attribs { [string]: string }
+--- @field [integer] xml_element
+local xml_node = {};
+xml_node.__index = xml_node;
+
+--- @param name string
+function xml_node:get_all(name)
+ --- @type xml_node[]
+ local res = {};
+
+ for _, el in ipairs(self) do
+ if type(el) ~= "string" and el.tag == name then
+ res[#res + 1] = el;
+ end
+ end
+
+ return res;
+end
+
+--- @param name string
+function xml_node:get(name)
+ local res = self:get_all(name);
+ if #res == 0 then
+ error("node '" .. name .. "' not found");
+ elseif #res > 1 then
+ error("multiple nodes '" .. name .. "' exist");
+ else
+ return res[1];
+ end
+end
+
+function xml_node:text()
+ if #self == 0 then
+ return "";
+ elseif #self == 1 and type(self[1]) == "string" then
+ return self[1];
+ else
+ error "not a text-only node";
+ end
+end
+
+--- @param raw xml_node_raw
+--- @return xml_node
+function xml_node.new(raw)
+ local res = setmetatable({
+ tag = raw.tag,
+ attribs = raw.attribs or {},
+ }, xml_node);
+
+ table.move(raw, 1, #raw, 1, res);
+
+ return res;
+end
+
+local function skip_spaces(raw, i)
+ local next_i = raw:find("[^%s]", i);
+ if next_i then return next_i end
+
+ if raw:find("^%s", i) then
+ local match = raw:match("^%s*", i);
+ if match then return i + #match end
+ else
+ return i;
+ end
+end
+
+local function parse_tag(raw, i)
+ i = skip_spaces(raw, i);
+
+ local tag = raw:match("^[%w0-9%-]+", i);
+ if tag == nil then error("expected tag name near '" .. raw:sub(i, i + 25) .. "'") end
+
+ i = i + #tag;
+
+ local attribs = {};
+
+ while true do
+ i = skip_spaces(raw, i);
+
+ local all, key, _, val = raw:match("^(([%w0-9%-]-)%s*=%s*(['\"])(.-)%3%s*)", i);
+ if all then
+ attribs[key] = val;
+ i = i + #all;
+ else
+ break;
+ end
+ end
+
+ return { tag = tag, attribs = attribs }, i;
+end
+
+local function parse_part(raw, i, allow_version)
+ i = skip_spaces(raw, i);
+
+ repeat
+ local comment_end;
+
+ if raw:sub(i, i + 3) == "", i);
+ i = comment_end or #raw;
+ i = skip_spaces(raw, i);
+ end
+ until not comment_end;
+
+ if i > #raw then
+ return { type = "eof" }, i;
+ elseif allow_version and raw:sub(i, i + 1) == "" then
+ i = i + 2;
+
+ local tag;
+ tag, i = parse_tag(raw, i);
+
+ if raw:sub(i, i + 1) ~= "?>" then
+ error("malformed XML near '" .. raw:sub(i, i + 25) .. "'");
+ end
+ i = i + 2;
+
+ return { type = "version", tag = tag.tag, attribs = tag.attribs }, i;
+ elseif raw:sub(i, i + 1) == "" then
+ i = i + 2;
+ i = skip_spaces(raw, i);
+
+ local tag = raw:match("[%w0-9%-]+", i);
+ if tag == nil then error("expected closing tag name near '" .. raw:sub(i, i + 25) .. "'") end
+
+ i = i + #tag;
+
+ if raw:sub(i, i) == ">" then
+ i = i + 1;
+ return { type = "end", tag = tag }, i;
+ else
+ error("malformed closing tag near '" .. raw:sub(i, i + 25) .. "'");
+ end
+
+ elseif raw:sub(i, i) == "<" then
+ i = i + 1;
+
+ local tag;
+ tag, i = parse_tag(raw, i);
+
+
+ if raw:sub(i, i + 1) == "/>" then
+ i = i + 2;
+ return { type = "small", tag = tag.tag, attribs = tag.attribs }, i;
+ elseif raw:sub(i, i) == ">" then
+ i = i + 1;
+ return { type = "begin", tag = tag.tag, attribs = tag.attribs }, i;
+ else
+ error("malformed opening tag near '" .. raw:sub(i, i + 25) .. "'");
+ end
+ else
+ local text_parts = {};
+
+ while i <= #raw do
+ local text_end = raw:find("<", i);
+
+ local text_part = raw:sub(i, text_end and text_end - 1 or #raw):match "^%s*(.-)%s*$";
+ if text_part ~= "" then
+ text_parts[#text_parts + 1] = text_part;
+ end
+
+ if not text_end then break end
+
+ i = text_end or #raw;
+
+ local comment_end;
+
+ if raw:sub(i, i + 3) == "", i);
+ i = comment_end and comment_end + 1 or #raw;
+ i = skip_spaces(raw, i);
+ else
+ break
+ end
+ end
+
+ if #text_parts > 0 then
+ return { type = "text", text = table.concat(text_parts, " ") }, i;
+ elseif i > #raw then
+ return { type = "eof" }, i;
+ else
+ error("malformed XML near '" .. raw:sub(i, i + 25) .. "'");
+ end
+ end
+
+end
+
+--- @param raw string
+--- @return xml_element
+local function parse(raw)
+ --- @type xml_node
+ local curr_node = xml_node.new { tag = "document", attribs = {} };
+ local stack = { curr_node };
+ local i = 1;
+ local first = true;
+
+ while true do
+ local part;
+ part, i = parse_part(raw, i, first);
+ if part.type == "eof" then break end
+
+ first = false;
+
+ if part.type == "text" then
+ if #stack == 1 then
+ error "text may not appear outside a tag";
+ else
+ curr_node[#curr_node + 1] = part.text;
+ end
+ elseif part.type == "version" then
+ curr_node.attribs.type = part.tag;
+ curr_node.attribs.version = part.attribs.version;
+ elseif part.type == "begin" then
+ local new_node = xml_node.new { tag = part.tag, attribs = part.attribs };
+ curr_node[#curr_node + 1] = new_node;
+ curr_node = new_node;
+ stack[#stack + 1] = new_node;
+ elseif part.type == "end" then
+ if part.tag ~= curr_node.tag then
+ error("closing tag '" .. part.tag .. "' doesn't match most recent opening tag '" .. curr_node.tag .. "'");
+ else
+ table.remove(stack);
+ curr_node = stack[#stack];
+ end
+ elseif part.type == "small" then
+ curr_node[#curr_node + 1] = xml_node.new { tag = part.tag, attribs = part.attribs };
+ else
+ error "wtf";
+ end
+ end
+
+ if #stack > 1 then
+ error("tag '" .. curr_node.tag .. "' was left open");
+ end
+
+ return curr_node;
+end
+
+return {
+ parse = parse,
+ node = xml_node.new,
+};
diff --git a/src/init.lua b/src/init.lua
new file mode 100644
index 0000000..774c540
--- /dev/null
+++ b/src/init.lua
@@ -0,0 +1,16 @@
+require "util.printing";
+
+local actions = {
+ help = require "cli.help",
+ query = require "cli.query",
+ dump = require "cli.dump",
+};
+
+return function (action, ...)
+ if action == "--help" or action == "-h" or action == "-help" then
+ return actions.help();
+ end
+
+ return actions[action](...);
+end
+
diff --git a/src/ostree.lua b/src/ostree.lua
new file mode 100644
index 0000000..5c574af
--- /dev/null
+++ b/src/ostree.lua
@@ -0,0 +1,251 @@
+local gvariant = require "formats.gvariant";
+local fmt = require "fmt";
+local zlib = require "zlib";
+
+local header_fmt = fmt.new ">! I4 I4";
+
+--- OSTREE_COMMIT_GVARIANT_FORMAT:
+--- - a{sv}: metadata
+--- - r: previous commit checksum
+--- - a(sr): related objects
+--- - s: subject
+--- - s: body
+--- - t: timestamp
+--- - r: root dir ref
+--- - r: root dirmeta ref
+local read_commit = gvariant "a{sv}ra(sr)sstrr";
+
+--- OSTREE_TREE_GVARIANT_FORMAT:
+--- - a(sr): a map of filename to its file ref
+--- - a(srr): a map of dirname to its dir ref and dirmeta ref
+local read_dir = gvariant "a(sr)a(srr)";
+
+--- OSTREE_DIRMETA_GVARIANT_FORMAT:
+--- - u: uid (big-endian)
+--- - u: gid (big-endian)
+--- - u: mode (big-endian)
+--- - a(ayay): xattrs
+local read_dirmeta = gvariant "uuua(ayay)";
+
+--- - t: size
+--- - u: uid
+--- - u: gid
+--- - u: mode
+--- - u: rdev
+--- - s: symlink_target
+--- - a(ayay): xattrs
+local read_header = gvariant "tuuuusa(ayay)";
+
+--- OSTREE_SUMMARY_GVARIANT_FORMAT:
+--- - a{s(tra{sv})}: map of ref name to data about it
+--- - t: commit size
+--- - r: checksum
+--- - a{sv}: metadata
+--- - a{sv}: metadata
+local read_summary = gvariant "a{s(tra{sv})}a{sv}";
+
+--- Flatpak-specific format (I think):
+--- - a{s(rara{sv})}: map of architecture name to data about it
+--- - r: checksum
+--- - ar: history of previous checksums
+--- - a{sv}: metadata
+--- - a{sv}: metadata
+local read_summary_index = gvariant "a{s(rara{sv})}a{sv}";
+
+--- @class ostree
+--- @field reader fun(path: string): string
+local ostree = {};
+ostree.__index = ostree;
+
+--- @param reader fun(path: string): string
+--- @return ostree
+function ostree.new(reader)
+ return setmetatable({ reader = reader }, ostree);
+end
+--- @param url string
+--- @return ostree
+function ostree.from_http(url)
+ local http = require "http";
+
+ return ostree.new(function (path)
+ return http.get(url .. "/" .. path);
+ end);
+end
+--- @param root string
+--- @return ostree
+function ostree.from_fs(root)
+ return ostree.new(function (path)
+ local f = assert(io.open(root .. "/" .. path));
+ local content = assert(f:read "*a");
+ assert(f:close());
+
+ return content;
+ end);
+end
+
+--- @param data string
+function ostree.parse_commit(data)
+ local metadata, prev, related, subject, body, timestamp, root_dir, rood_dirmeta = read_commit(data);
+
+ return {
+ metadata = metadata,
+ prev = prev,
+ related = related,
+ subject = subject,
+ body = body,
+ timestamp = timestamp,
+ root = { root_dir, rood_dirmeta },
+ };
+end
+--- @param dir string | { [1]: string, [2]?: string }
+--- @param meta? string
+function ostree.parse_dir(dir, meta)
+ if type(dir) ~= "string" then
+ meta = dir[2];
+ dir = dir[1];
+ end
+
+ local files_arr, dirs_arr = read_dir(dir);
+
+ local files = {};
+ local dirs = {};
+
+ for i = 1, #files_arr do
+ files[files_arr[i][1]] = files_arr[i][2];
+ end
+ for i = 1, #dirs_arr do
+ dirs[dirs_arr[i][1]] = { dirs_arr[i][2], dirs_arr[i][3] };
+ end
+
+ if meta then
+ local uid, gid, mode, xattrs = read_dirmeta(meta);
+ return { type = "dir", files = files, dirs = dirs, uid = uid, gid = gid, mode = mode, xattrs = xattrs };
+ else
+ return { type = "dir", files = files, dirs = dirs };
+ end
+end
+--- @param data string
+function ostree.parse_file(data)
+ local header_size, idk = header_fmt:unpack(data);
+ local header = data:sub(9, header_size + 8);
+ local size, uid, gid, mode, rdev, link, xattrs = read_header(header);
+
+ if link ~= "" then
+ if size > 0 then
+ error "link may not have contents";
+ else
+ return {
+ type = "link",
+ target = link,
+ uid = uid,
+ gid = gid,
+ mode = mode,
+ xattrs = xattrs,
+ };
+ end
+ else
+ local get_content;
+ if size == 0 then
+ function get_content()
+ return function ()
+ return nil;
+ end
+ end
+ else
+ function get_content()
+ local compressed = data:sub(header_size + 9);
+ return zlib.inflate_raw(compressed);
+ end
+ end
+
+ return {
+ type = "file",
+ get_content = get_content,
+ uid = uid,
+ gid = gid,
+ mode = mode,
+ size = size,
+ xattrs = xattrs,
+ };
+ end
+end
+--- @param data string
+function ostree.parse_summary(data)
+ local refs, metadata = read_summary(data);
+
+ for key, val in pairs(refs) do
+ refs[key] = {
+ size = val[1],
+ checksum = val[2],
+ metadata = val[3],
+ };
+ end
+
+ return {
+ refs = refs,
+ metadata = metadata,
+ };
+end
+--- @param data string
+function ostree.parse_subsummary(data)
+ local parts = {};
+ for part in zlib.gunzip(data) do parts[#parts + 1] = part end
+ return ostree.parse_summary(table.concat(parts));
+end
+--- @param data string
+function ostree.parse_summary_index(data)
+ local refs, metadata = read_summary_index(data);
+
+ for key, val in pairs(refs) do
+ refs[key] = {
+ checksum = val[1],
+ -- history = val[2],
+ metadata = val[3],
+ };
+ end
+
+ return {
+ refs = refs,
+ metadata = metadata,
+ };
+end
+
+--- @param type string
+--- @param ref string
+function ostree:read_object(type, ref)
+ return self.reader("objects/" .. ref:sub(1, 2) .. "/" .. ref:sub(3) .. "." .. type);
+end
+
+--- @param ref string
+function ostree:read_commit(ref)
+ return ostree.parse_commit(self:read_object("commit", ref));
+end
+--- @param dir string | { [1]: string, [2]?: string }
+--- @param meta? string
+function ostree:read_dir(dir, meta)
+ if type(dir) ~= "strign" then
+ dir = dir[1];
+ meta = dir[2];
+ end
+
+ return ostree.parse_dir(self:read_object("dirtree", dir), meta and self:read_object("dirmeta", meta));
+end
+--- @param ref string
+function ostree:read_file(ref)
+ return ostree.parse_file(self:read_object("filez", ref));
+end
+function ostree:read_summary()
+ return ostree.parse_summary(self.reader("summary"));
+end
+function ostree:read_subsummary(ref)
+ return ostree.parse_subsummary(self.reader("summaries/" .. ref .. ".gz"));
+end
+function ostree:read_summary_index()
+ return ostree.parse_summary_index(self.reader("summary.idx"));
+end
+--- @param pkg string
+function ostree:read_ref(pkg)
+ return self.reader("refs/heads/" .. pkg):gsub("%s", "");
+end
+
+return ostree;
diff --git a/src/util/args.lua b/src/util/args.lua
new file mode 100644
index 0000000..74589aa
--- /dev/null
+++ b/src/util/args.lua
@@ -0,0 +1,106 @@
+---@param consumers table
+--- @param ... string
+--- @returns nil
+return function (consumers, ...)
+ local consumer_stack = {};
+ local pass_args = false;
+
+ local function digest_arg(v)
+ local consumed = false;
+
+ while true do
+ local el = table.remove(consumer_stack);
+ if el == nil then break end
+
+ local res, fin = el(v);
+ if res then
+ consumed = true;
+ end
+ if fin then
+ pass_args = true;
+ break;
+ end
+
+ if fin or res then
+ break;
+ end
+ end
+
+ if not consumed then
+ if consumers[1](v) then
+ pass_args = true;
+ end
+ end
+ end
+
+ local function get_consumer(name)
+ local consumer = name;
+ local path = {};
+ local n = 0;
+
+ while true do
+ local curr = consumer;
+ if path[curr] then
+ local path_arr = {};
+
+ for k, v in next, path do
+ path_arr[v] = k;
+ end
+
+ error("Alias to '" .. curr .. "' is recursive: " .. table.concat(path_arr, " -> "));
+ end
+
+ consumer = consumers[curr];
+ if consumer == nil then
+ local prefix;
+ if n == 0 then
+ prefix = "Unknown flag";
+ else
+ prefix = "Unknown alias";
+ end
+
+ if #curr == 1 then
+ error(prefix .. " '-" .. curr .. "'");
+ else
+ error(prefix .. " '--" .. curr .. "'");
+ end
+ elseif type(consumer) == "function" then
+ return consumer;
+ end
+
+ path[curr] = n;
+ n = n + 1;
+ end
+ end
+
+ local function next(...)
+ if ... == nil then
+ while #consumer_stack > 0 do
+ local el = table.remove(consumer_stack);
+
+ if el(nil) then
+ error("Unexpected end of arguments", 0);
+ break;
+ end
+ end
+ else
+ if pass_args then
+ consumers[1]((...));
+ elseif ... == "--" then
+ pass_args = true;
+ elseif (...):match "^%-%-" then
+ consumer_stack[#consumer_stack + 1] = get_consumer((...):sub(3));
+ elseif (...):match "^%-" then
+ for c in (...):sub(2):gmatch "." do
+ table.insert(consumer_stack, 1, get_consumer(c));
+ end
+ else
+ digest_arg(...);
+ end
+
+ return next(select(2, ...));
+ end
+ end
+
+ return next(...);
+end
diff --git a/src/util/printing.lua b/src/util/printing.lua
new file mode 100644
index 0000000..cd65224
--- /dev/null
+++ b/src/util/printing.lua
@@ -0,0 +1,128 @@
+local function escape_str(s)
+ local in_char = { "\\", "\"", "/", "\b", "\f", "\n", "\r", "\t" };
+ local out_char = { "\\", "\"", "/", "b", "f", "n", "r", "t" };
+ for i, c in ipairs(in_char) do
+ s = s:gsub(c, "\\" .. out_char[i]);
+ end
+ return s
+end
+
+local function stringify_impl(obj, indent_str, n, passed)
+ local parts = {};
+ local kind = type(obj);
+
+ if kind == "table" then
+ if passed[obj] then return "" end
+ passed[obj] = true;
+
+ local len = #obj;
+
+ for i = 1, len do
+ parts[i] = stringify_impl(obj[i], indent_str, n + 1, passed) .. ",";
+ end
+
+ local keys = {};
+
+ for k, v in pairs(obj) do
+ if type(k) ~= "number" or k > len then
+ keys[#keys + 1] = { k, v };
+ end
+ end
+
+ table.sort(keys, function (a, b)
+ if type(a[1]) == "number" and type(b[1]) == "number" then
+ return a[1] < b[1];
+ elseif type(a[1]) == "string" and type(b[1]) == "string" then
+ return a[1] < b[1];
+ else
+ return type(a[1]) < type(b[1]) or tostring(a[1]) < tostring(b[1]);
+ end
+ end);
+
+ for i = 1, #keys do
+ local k = keys[i][1];
+ local v = keys[i][2];
+
+ local val = stringify_impl(v, indent_str, n + 1, passed);
+ if val ~= nil then
+ if type(k) == "string" then
+ parts[#parts + 1] = table.concat { k, " = ", val, "," };
+ else
+ parts[#parts + 1] = table.concat { "[", stringify_impl(k, indent_str, n + 1, passed), "] = ", val, "," };
+ end
+ end
+ end
+
+ local meta = getmetatable(obj);
+
+ passed[obj] = false;
+
+ if #parts == 0 then
+ return "{}";
+ end
+
+ local contents = table.concat(parts, " "):sub(1, -2);
+ if #contents > 80 then
+ local indent = "\n" .. string.rep(indent_str, n + 1);
+
+ contents = table.concat {
+ indent,
+ table.concat(parts, indent),
+ "\n", string.rep(indent_str, n)
+ };
+ else
+ contents = " " .. contents .. " ";
+ end
+
+ return table.concat { "{", contents, "}" };
+ elseif kind == "string" then
+ return "\"" .. escape_str(obj) .. "\"";
+ elseif kind == "function" then
+ local data = debug.getinfo(obj, "S");
+ return table.concat { tostring(obj), " @ ", data.short_src, ":", data.linedefined };
+ elseif kind == "nil" then
+ return "nil";
+ else
+ return tostring(obj);
+ end
+end
+
+--- Turns the given value to a human-readable string.
+--- Should be used only for debugging and display purposes
+local function to_readable(obj, indent_str)
+ return stringify_impl(obj, indent_str or " ", 0, {});
+end
+
+local function print(...)
+ for i = 1, select("#", ...) do
+ if i > 1 then
+ io.stderr:write("\t");
+ end
+
+ io.stderr:write(tostring((select(i, ...))));
+ end
+
+ io.stderr:write("\n");
+end
+
+--- Prints the given values in a human-readable manner to stderr
+--- Should be used only for debugging
+local function pprint(...)
+ if select("#", ...) == 0 then
+ io.stderr:write "\n";
+ else
+ for i = 1, select("#", ...) do
+ if i > 1 then
+ io.stderr:write "\t";
+ end
+
+ io.stderr:write(to_readable((select(i, ...))));
+ end
+
+ io.stderr:write "\n";
+ end
+end
+
+_G.to_readable = to_readable;
+_G.print = print;
+_G.pprint = pprint;
]