commit cbb9b5805a4d560285dfd4f9c13fd928a33d311c 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..1f9d6c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/* +!/lib +!/mod +!/src +!/.editorconfig +!/.gitignore +!/Makefile 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 + 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 + 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;