initial commit

This commit is contained in:
TopchetoEU 2025-03-08 23:22:10 +02:00
commit 7571cfbca7
Signed by: topchetoeu
GPG Key ID: 6531B8583E5F6ED4
24 changed files with 2612 additions and 0 deletions

5
.editorconfig Normal file
View File

@ -0,0 +1,5 @@
[*.?ts]
end_of_line = lf
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
/*
!/lib
!/mod
!/src
!/.editorconfig
!/.gitignore
!/Makefile
!/LICENSE

15
LICENSE Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.

16
Makefile Normal file
View File

@ -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

50
lib/build.lua Normal file
View File

@ -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, ...), "@<entry>", "t", nil)));
local f = assert(io.open(out, "w"));
f:write "#include <stdint.h>\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(...);

771
lib/fmt.c Normal file
View File

@ -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 <lauxlib.h>
#include <lua.h>
#include <stdbool.h>
#include <string.h>
#include <stdlib.h>
#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;
}

101
lib/http.c Normal file
View File

@ -0,0 +1,101 @@
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include <curl/curl.h>
#include <stdlib.h>
#include <string.h>
#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;
}

5
lib/lib.h Normal file
View File

@ -0,0 +1,5 @@
#include <lua.h>
int http_open_lib(lua_State *ctx);
int fmt_open_lib(lua_State *ctx);
int zlib_open_lib(lua_State *ctx);

126
lib/main.c Normal file
View File

@ -0,0 +1,126 @@
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include <curl/curl.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#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), "<main>", "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), "<main>", "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;
}

136
lib/zlib.c Normal file
View File

@ -0,0 +1,136 @@
#include <zlib.h>
#include <lauxlib.h>
#include <lua.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#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;
}

24
mod/fmt.d.lua Normal file
View File

@ -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;

9
mod/http.d.lua Normal file
View File

@ -0,0 +1,9 @@
--- @meta http
local http = {};
--- @param path string
--- @return string body
function http.get(path) end
return http;

17
mod/zlib.d.lua Normal file
View File

@ -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;

41
src/cli/dump.lua Normal file
View File

@ -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

14
src/cli/help.lua Normal file
View File

@ -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 <term> - searches the given term in the current repo";
print "dump <ref> [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

28
src/cli/query.lua Normal file
View File

@ -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

34
src/errfunc.lua Normal file
View File

@ -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 <C>";
elseif location:find "%[string" then
location = "at <string>";
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;

401
src/formats/gvariant.lua Normal file
View File

@ -0,0 +1,401 @@
-- TODO: remove string.unpack
local fmt = require "fmt";
local uint8_le_fmt = fmt.new "<! I1";
local uint16_le_fmt = fmt.new "<! I2";
local uint32_le_fmt = fmt.new "<! I4";
local parsers = {};
local cache = setmetatable({}, { __mode = "v" });
-- local indent = "";
local function alignof(i, n)
return math.ceil((i - 1) / n) * n + 1;
end
local function offset_reader(s, e)
if e - s + 1 < 256 then
return function (buf, i)
local res = uint8_le_fmt:unpack(buf, e - i + 1);
return res;
end, 1;
elseif e - s + 1 < 65536 then
return function (buf, i)
local res = uint16_le_fmt:unpack(buf, e - i * 2 + 1);
return res;
end, 2;
else
return function (buf, i)
local res = uint32_le_fmt:unpack(buf, e - i * 4 + 1);
return res;
end, 4;
end
end
local function construct_reader_no_cache(str, i)
local c = str:sub(i, i);
if c == "b" then
return parsers.bool(), i + 1;
elseif c == "y" then
return parsers.uint8(), i + 1;
elseif c == "n" then
return parsers.int16(), i + 1;
elseif c == "q" then
return parsers.uint16(), i + 1;
elseif c == "i" then
return parsers.int32(), i + 1;
elseif c == "u" then
return parsers.uint32(), i + 1;
elseif c == "x" then
return parsers.int64(), i + 1;
elseif c == "t" then
return parsers.uint64(), i + 1;
elseif c == "d" then
return parsers.float64(), i + 1;
elseif c == "s" or c == "o" or c == "g" then
return parsers.string(), i + 1;
elseif c == "r" then
return parsers.ref(), i + 1;
elseif c == "v" then
return parsers.variant(), i + 1;
elseif c == "m" then
local reader;
reader, i = construct_reader_no_cache(str, i + 1);
return parsers.maybe(reader), i;
elseif c == "a" then
i = i + 1;
if str:sub(i, i) == "{" then
i = i + 1;
local key, val;
key, i = construct_reader_no_cache(str, i);
val, i = construct_reader_no_cache(str, i);
if str:sub(i, i) ~= "}" then
error "malformed gvarian format";
end
i = i + 1;
return parsers.map(key, val), i;
elseif str:sub(i, i) == "y" then
i = i + 1;
return parsers.buffer(), i;
else
local reader;
reader, i = construct_reader_no_cache(str, i);
return parsers.array(reader), i;
end
elseif c == "(" then
i = i + 1;
local tuple_parsers = {};
while str:sub(i, i) ~= ")" do
tuple_parsers[#tuple_parsers + 1], i = construct_reader_no_cache(str, i);
end
i = i + 1;
return parsers.tuple(table.unpack(tuple_parsers)), i;
elseif c == "{" then
i = i + 1;
local key, val;
key, i = construct_reader_no_cache(str, i);
val, i = construct_reader_no_cache(str, i);
if str:sub(i, i) ~= "}" then
error "malformed gvarian format";
end
i = i + 1;
return parsers.tuple(key, val), i;
else
error "malformed gvariant format";
end
end
local function construct_reader(str)
if not cache[str] then
cache[str] = { construct_reader_no_cache(str, 1) };
end
return cache[str][1], cache[str][2];
end
local function encoded_helper(c)
return ("%02x"):format(string.byte(c));
end
local function to_encoded(str)
return str:gsub(".", encoded_helper);
end
local function fmt_to_parser(format, fixed, align)
return function ()
return {
fixed = fixed, align = align,
read = function(buf, s)
return (format:unpack(buf, s));
end,
};
end
end
parsers.uint8 = fmt_to_parser(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

59
src/formats/ini.lua Normal file
View File

@ -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,
};

251
src/formats/xml.lua Normal file
View File

@ -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) == "<!--" then
i = i + 43;
_, comment_end = raw:find("-->", 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) == "<!--" then
i = i + 4;
_, comment_end = raw:find("-->", 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,
};

16
src/init.lua Normal file
View File

@ -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

251
src/ostree.lua Normal file
View File

@ -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;

106
src/util/args.lua Normal file
View File

@ -0,0 +1,106 @@
---@param consumers table<string, (fun(arg: string): boolean?, boolean?) | string>
--- @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

128
src/util/printing.lua Normal file
View File

@ -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 "<circular>" 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 "<nothing>\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;