diff --git a/lib/build.lua b/lib/build.lua index 3721754..e8218b8 100644 --- a/lib/build.lua +++ b/lib/build.lua @@ -11,17 +11,35 @@ end local function all_loader(paths, ...) local files = { ... }; - local function coro_entry() + + return coroutine.wrap(function () 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); + coroutine.yield("require 'init' (...);"); + end); +end + +local function escape(str) + return "\"" .. str:gsub(".", function (c) + local b = string.byte(c); + + if c == "\n" then + return "\\n"; + elseif c == "\\" then + return "\\\\"; + elseif c == "\"" then + return "\\\""; + elseif b >= 32 and b <= 126 then + return c; + else + return ("\\%.3o"):format(b); + end + end) .. "\""; end local function main(root, out, ...) @@ -36,13 +54,15 @@ local function main(root, out, ...) 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[] = { "; + f:write "#define BYTECODE_SIZE (sizeof entry_bytecode - 1)\n"; + f:write "static const uint8_t entry_bytecode[] = "; - for i = 1, #res do - f:write(string.byte(res, i), ", "); + for i = 1, math.ceil(#res / 64) do + f:write(escape(res:sub((i - 1) * 64 + 1, i * 64))); + f:write"\n"; end - f:write "};"; + f:write ";"; assert(f:close()); end diff --git a/src/errfunc.lua b/lib/errfunc.lua similarity index 100% rename from src/errfunc.lua rename to lib/errfunc.lua diff --git a/lib/fs.c b/lib/fs.c new file mode 100644 index 0000000..e78777c --- /dev/null +++ b/lib/fs.c @@ -0,0 +1,125 @@ +#define _GNU_SOURCE + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "lib.h" + +static const char *fs_check_path(lua_State *ctx, int i, size_t *psize) { + size_t n; + const char *path = luaL_checklstring(ctx, 1, &n); + + size_t real_len = strlen(path); + if (n != real_len) { + luaL_error(ctx, "path may not contain \\0"); + return NULL; + } + + if (psize) *psize = n; + + return path; +} + +static int lib_fs_mkdir(lua_State *ctx) { + size_t path_size; + const char *ro_path = fs_check_path(ctx, 1, &path_size); + bool recursive = lua_toboolean(ctx, 2); + + if (recursive) { + char path[path_size + 1]; + memcpy(path, ro_path, path_size + 1); + + for (size_t i = 0; i <= path_size ; i++) { + if (path[i] == '/' || path[i] == '\0') { + path[i] = '\0'; + + if (mkdir(path, 0755) < 0 && errno != EEXIST) { + lua_pushnil(ctx); + lua_pushfstring(ctx, "can't make directory: %s", strerror(errno)); + return 12; + } + + if (i < path_size) path[i] = '/'; + } + } + } + else { + if (mkdir(ro_path, 0755) < 0) { + lua_pushnil(ctx); + lua_pushfstring(ctx, "can't make directory: %s", strerror(errno)); + return 12; + } + } + + lua_pushboolean(ctx, true); + return 1; +} +static int lib_fs_symlink(lua_State *ctx) { + const char *src = fs_check_path(ctx, 1, NULL); + const char *dst = fs_check_path(ctx, 2, NULL); + + if (symlink(src, dst) < 0) { + lua_pushnil(ctx); + lua_pushfstring(ctx, "failed to chmod: %s", strerror(errno)); + return 2; + } + + lua_pushboolean(ctx, true); + return 1; +} +static int lib_fs_chmod(lua_State *ctx) { + const char *path = fs_check_path(ctx, 1, NULL); + int mode; + + if (lua_isinteger(ctx, 2)) { + mode = lua_tointeger(ctx, 2); + mode = mode & 0xFFF; + } + else { + const char *strmode = luaL_checkstring(ctx, 2); + + mode = 0; + + for (size_t i = 0; strmode[i]; i++) { + char c = strmode[i]; + if (c >= '0' && c <= '7') { + mode <<= 3; + mode |= c - '0'; + } + else { + return luaL_error(ctx, "invalid mode '%s' - must be an octal integer", strmode); + } + } + } + + if (chmod(path, mode) < 0) { + lua_pushnil(ctx); + lua_pushfstring(ctx, "failed to chmod: %s", strerror(errno)); + return 2; + } + + lua_pushboolean(ctx, true); + return 1; +} + +int fs_open_lib(lua_State *ctx) { + luaL_newlib(ctx, ((luaL_Reg[]) { + { "mkdir", lib_fs_mkdir }, + { "chmod", lib_fs_chmod }, + { "symlink", lib_fs_symlink }, + { NULL, NULL }, + })); + + return 1; +} diff --git a/lib/http.c b/lib/http.c index 9228391..a12b059 100644 --- a/lib/http.c +++ b/lib/http.c @@ -87,6 +87,7 @@ static int lib_http_get(lua_State *ctx) { char *body = body_compress(buff, &size); lua_pushlstring(ctx, body, size); free(body); + // fprintf(stderr, "HTTP %s\n", url); return 1; } diff --git a/lib/lib.h b/lib/lib.h index 9c5a6e3..914aae0 100644 --- a/lib/lib.h +++ b/lib/lib.h @@ -3,3 +3,4 @@ int http_open_lib(lua_State *ctx); int fmt_open_lib(lua_State *ctx); int zlib_open_lib(lua_State *ctx); +int fs_open_lib(lua_State *ctx); diff --git a/lib/main.c b/lib/main.c index 5f459ef..71f2de9 100644 --- a/lib/main.c +++ b/lib/main.c @@ -9,108 +9,35 @@ #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, - }; + static uint8_t entry_bytecode[] = "error \"bad build\""; #define BYTECODE entry_bytecode + #define BYTECODE_SIZE (sizeof entry_bytecode - 1) #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 char err_bytecode[] = + "local err = ...;" + "local trace = debug.traceback(nil, 2):match \"^[^\\n]+\\n(.+)$\"" + "print(table.concat { \"unhandled error: \", err, \"\\n\", trace })"; 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); + luaL_requiref(ctx, "fs", fs_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")) { + if (luaL_loadbuffer(ctx, err_bytecode, sizeof err_bytecode - 1, "
")) { 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")) { + if (luaL_loadbuffer(ctx, (char*)BYTECODE, BYTECODE_SIZE, "
")) { fprintf(stderr, "error while loading lua bytecode for main: %s", lua_tostring(ctx, -1)); return 1; } diff --git a/mod/fs.d.lua b/mod/fs.d.lua new file mode 100644 index 0000000..2d3505c --- /dev/null +++ b/mod/fs.d.lua @@ -0,0 +1,23 @@ +--- @meta fs + +local fs = {}; + +--- @param path string +--- @param recursive? boolean +--- @return boolean? ok +--- @return string? err +function fs.mkdir(path, recursive) end + +--- @param src string +--- @param dst string +--- @return boolean? ok +--- @return string? err +function fs.symlink(src, dst) end + +--- @param path string +--- @param mode string | integer +--- @return boolean? ok +--- @return string? err +function fs.chmod(path, mode) end + +return fs; diff --git a/src/cli/dump.lua b/src/cli/dump.lua index 915c7e7..331a8ce 100644 --- a/src/cli/dump.lua +++ b/src/cli/dump.lua @@ -1,41 +1,13 @@ 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); +return function (options, name, out) + local repo = ostree.from_http(options.repo); local ref = repo:read_ref(name); local commit = repo:read_commit(ref); - local function dump_file(path, file) - print("Dumping " .. path .. "..."); + local root = repo:read_dir(commit.root); - 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); + root:recurse(repo, function (path, node) + return ostree.dump_node(out .. path, node); + end); end diff --git a/src/cli/pack.lua b/src/cli/pack.lua new file mode 100644 index 0000000..28ab3a9 --- /dev/null +++ b/src/cli/pack.lua @@ -0,0 +1,164 @@ +local ostree = require "ostree"; +local flatpak = require "flatpak"; +local fs = require "fs"; + +local loader = [[#!/bin/sh + +set -euo pipefail; + +cleanup() { + cd /; + + if [ "${mountpoint_dir+x}" ]; then + umount "$mountpoint_dir"; + fi + if [ "${squashfs_dir+x}" ]; then + umount "$squashfs_dir"; + fi + if [ "${tmp_dir+x}" ]; then + rm -rf "$tmp_dir"; + fi +} + +jail_run() { + unshare -cR $mountpoint_dir $@; +} + +trap cleanup EXIT; + +marker="#END_OF_"LOADER"_MARKER"; + +offset=$(( $(grep -abo "$marker" "$PWD/$0" | head -n 1 | cut -d: -f1) + ${#marker} + 1 )); + +if [ "${1-}" = "--slimpack-offset" ] || [ "${1-}" = "--appimage-offset" ]; then + echo $offset; + exit 0; +fi + +tmp_dir=$(mktemp -d /tmp/.slimpack.XXXXXXXXXXXX); + +mkdir "$tmp_dir/squashfs"; +squashfuse -o offset=$offset "$PWD/$0" "$tmp_dir/squashfs"; +squashfs_dir="$tmp_dir/squashfs"; + +mkdir "$tmp_dir/mount_point"; +unionfs -o suid,dev "$squashfs_dir"=RO:/=RW "$tmp_dir/mount_point"; +mountpoint_dir="$tmp_dir/mount_point"; + +if [ "${1-}" = "--slimpack-dbg" ]; then + jail_run /bin/bash; +elif [ "${1-}" = "--slimpack-dbg-2" ]; then + cd "$mountpoint_dir" && bash; +else + jail_run "/entry" $@; +fi +exit 0; + +#END_OF_LOADER_MARKER +]]; + +local entry_preifx = [[#!/bin/sh + +export PATH=$PATH:/app/bin +]]; + +local function help() + print "Slimpack pack help:"; + print "Subcommand of slimpack, used to repack a flatpak application into"; + print "an AppImage-like self-contained executable."; + print " --help: Shows this message"; + print " --target (-o): Specify the output file. Defaults to 'app'"; + print " : Specifies a package to be converted. At the moment, one may be specified."; + print " in the future however, extensions will be specified as more package refs"; + + os.exit(); +end + +local function dump_at(repo, name, target) + local ref = repo:read_ref(name); + local commit = repo:read_commit(ref); + + assert(repo:read_dir(commit.root)) + :dir(repo, "files") + :recurse(repo, function (path, node) + print(path); + return ostree.dump_node(target .. path, node); + end); +end + +local function write_entry(repo, name, target) + local meta = flatpak.parse_package(repo, name); + + local f = assert(io.open(target, "wb")); + f:write(entry_preifx); + f:write(meta.command); + f:close(); + assert(fs.chmod(target, "755")); +end + +local function write_res(img_path, target) + local f = assert(io.open(target, "wb")); + f:write(loader); + + local img_f = assert(io.open(img_path, "rb")); + + for part in img_f:lines(4096) do + f:write(part); + end + + img_f:close(); + f:close(); + assert(fs.chmod(target, "755")); +end + +return function (options, ...) + local target; + local name; + + require "util.args" ({ + function (val) + if name then + error "only one package may be specified (at the moment)"; + else + name = val; + return true; + end + end, + target = function (val) + if target then + error "target may be specified only once"; + else + target = val; + return true; + end + end, + help = help, + + o = "target", + }, ...); + + target = target or "./app"; + if not name then + error "package must be specified"; + end + + local repo = ostree.from_http(options.repo); + + local tmp_dir = assert(io.popen "mktemp -d .pack_XXXXXXXX":read "*a"):sub(1, -2); + + local ok, err = pcall(function () + dump_at(repo, name, tmp_dir .. "/dump/app"); + + write_entry(repo, name, tmp_dir .. "/dump/entry"); + + -- TODO: replace with C code + assert(os.execute(("mksquashfs %q %q"):format(tmp_dir .. "/dump", tmp_dir .. "/img.squashfs"))); + + write_res(tmp_dir .. "/img.squashfs", target); + end); + + -- TODO: replace with C code + os.execute(("rm -rf %q"):format(tmp_dir)); + + if not ok then error(err, 0) end +end diff --git a/src/cli/query.lua b/src/cli/query.lua index c962d81..bac94c5 100644 --- a/src/cli/query.lua +++ b/src/cli/query.lua @@ -1,28 +1,53 @@ local ostree = require "ostree"; -local repo_url = "https://dl.flathub.org/repo"; +local flatpak = require "flatpak"; local actions = {}; -function actions.list() - local repo = ostree.from_http(repo_url); +function actions.list(options) + local repo = ostree.from_http(options.repo); local index = repo:read_summary_index(); local summary = repo:read_subsummary(index.refs.x86_64.checksum); - for ref in pairs(summary.refs) do + for ref in summary.refs do print(ref); end end -function actions.search(term) - local repo = ostree.from_http(repo_url); +function actions.search(options, term) + local repo = ostree.from_http(options.repo); local index = repo:read_summary_index(); local summary = repo:read_subsummary(index.refs.x86_64.checksum); - for ref in pairs(summary.refs) do + for ref in summary.refs do if ref:lower():match(term:lower(), 1, true) then print(ref); end end end +function actions.info(options, name) + local repo = ostree.from_http(options.repo); + print(name); + local meta = flatpak.parse_package(repo, name); + pprint(meta); -return function (action, ...) - actions[action](...); + print("Name: " .. meta.name); + print("Branch: " .. meta.branch); + print("Architecture: " .. meta.arch); + print("Version: " .. (meta.version or "(not specified)")); + print("License: " .. (meta.license or "(not specified)")); + print("Summary: " .. (meta.summary or "(not specified)")); + + print("Dependencies:"); + if meta.deps.debug then + print("\tDebug package: " .. meta.deps.debug.name); + end + if meta.deps.locale then + print("\tLocale package: " .. meta.deps.locale.name); + end + for i = 1, #meta.deps do + local dep = meta.deps[i]; + print("\t" .. dep.name .. (dep.required and "" or " (optional)")); + end +end + +return function (options, action, ...) + actions[action](options, ...); end diff --git a/src/flatpak.lua b/src/flatpak.lua new file mode 100644 index 0000000..a0ed609 --- /dev/null +++ b/src/flatpak.lua @@ -0,0 +1,105 @@ +local ini = require "formats.ini"; +local xml = require "formats.xml"; + +local flatpak = {}; + +--- @param repo ostree +local function get_metainfo(repo, commit, metadata) + local root_dir = repo:read_dir(commit.root); + + local export_dir = root_dir:dir(repo, "export"); + if export_dir == nil then return nil end + + local share_dir = export_dir:dir(repo, "share"); + if share_dir == nil then return nil end + + local metainfo_dir = share_dir:dir(repo, "metainfo"); + if metainfo_dir == nil then return nil end + + local metainfo_ref = + metainfo_dir.files[metadata.Application.name .. ".metainfo.xml"] or + metainfo_dir.files[metadata.Application.name .. ".appdata.xml"]; + if metainfo_ref == nil then return nil end + + local metainfo_f = repo:read_file(metainfo_ref); + + local parts = {}; + for el in metainfo_f:get_content() do parts[#parts + 1] = el end + print(table.concat(parts, "")); + return xml.parse(table.concat(parts, "")); +end + +--- @param repo ostree +function flatpak.parse_package(repo, full_name) + local ref = repo:read_ref(full_name); + local commit = repo:read_commit(ref); + local name, arch, branch = full_name:match "^.-/(.-)/(.-)/(.-)$"; + print(arch, branch); + + local metadata = ini.parse(commit.metadata["xa.metadata"]); + local metainfo = get_metainfo(repo, commit, metadata); + + local res = {}; + + pprint(metadata); + + res.ref = full_name; + res.name = name; + res.arch = arch; + res.branch = branch; + + res.size = commit.metadata["xa.download-size"]; + res.command = metadata.Application.command; + res.runtime = metadata.Application.runtime; + res.deps = {}; + + for key, map in pairs(metadata) do + local dep = key:match "^Extension%s+(.-)%s*$"; + if dep then + dep = (dep .. "@"):match "^(.-)@"; + + local dep_ref = "runtime/" .. dep .. "/" .. arch .. "/" .. (map.version or branch); + local required = map["no-autodownload"] ~= "true"; + local mounts = {}; + + if map.directory then + mounts[#mounts + 1] = map.directory; + elseif map.directories then + mounts = ini.parse_list(map.directories); + end + + if dep:find "%.Debug$" then + res.deps.debug = { + name = dep_ref, + required = required, + mounts = mounts, + }; + elseif dep:find "%.Locale$" then + res.deps.locale = { + name = dep_ref, + required = required, + mounts = mounts, + }; + else + res.deps[#res.deps + 1] = { + name = dep_ref, + required = required, + mounts = mounts, + }; + end + + pprint(dep, map); + end + end + + if metainfo then + local app_meta = metainfo:get "component"; + res.version = app_meta:get "releases":get_all "release"[1].attribs.version; + res.license = app_meta:get "project_license":text(); + res.summary = app_meta:get_all "summary"[1]:text(); + end + + return res, commit; +end + +return flatpak; diff --git a/src/formats/gvariant.lua b/src/formats/gvariant.lua index 5b09616..acb9788 100644 --- a/src/formats/gvariant.lua +++ b/src/formats/gvariant.lua @@ -1,5 +1,3 @@ --- TODO: remove string.unpack - local fmt = require "fmt"; local uint8_le_fmt = fmt.new " 0), "offset calculation error"); + assert(n > 0, "negative array length"); n = math.floor(n); if as_iterable then diff --git a/src/formats/ini.lua b/src/formats/ini.lua index 15b8880..4d91f95 100644 --- a/src/formats/ini.lua +++ b/src/formats/ini.lua @@ -1,4 +1,6 @@ -local function parse_ini(raw, glob_group) +local ini = {}; + +function ini.parse(raw, glob_group) local lines = {}; for line in raw:gmatch "[^\n]+" do @@ -19,8 +21,9 @@ local function parse_ini(raw, glob_group) line_i = line_i + 1; line = line:match "^%s*(.-)%s*$"; + if line ~= "" then - local group = line.match "^%[%s*(.-)%s*%]$"; + local group = line:match "^%[%s*(.-)%s*%]$"; if group ~= nil then curr_group = {}; @@ -28,7 +31,7 @@ local function parse_ini(raw, glob_group) elseif curr_group == nil then error("line " .. line_i .. ": Unexpected global key"); else - local key, value = line.match "^%s*(.-)%s*=%s*(.-)%s*$"; + local key, value = line:match "^%s*(.-)%s*=%s*(.-)%s*$"; if key == nil then error("line " .. line_i .. ": Unexpected ini syntax"); end @@ -41,7 +44,7 @@ local function parse_ini(raw, glob_group) return groups; end -local function parse_ini_list(raw) +function ini.parse_list(raw) local res = {}; for el in raw:gmatch "%s*([^;]-)%s*" do @@ -53,7 +56,4 @@ local function parse_ini_list(raw) return res; end -return { - parse = parse_ini, - list = parse_ini_list, -}; +return ini; diff --git a/src/formats/xml.lua b/src/formats/xml.lua index bf80ac7..dec4761 100644 --- a/src/formats/xml.lua +++ b/src/formats/xml.lua @@ -72,7 +72,7 @@ end local function parse_tag(raw, i) i = skip_spaces(raw, i); - local tag = raw:match("^[%w0-9%-]+", 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; @@ -82,7 +82,7 @@ local function parse_tag(raw, i) while true do i = skip_spaces(raw, i); - local all, key, _, val = raw:match("^(([%w0-9%-]-)%s*=%s*(['\"])(.-)%3%s*)", i); + local all, key, _, val = raw:match("^(([%w0-9%-_:]-)%s*=%s*(['\"])(.-)%3%s*)", i); if all then attribs[key] = val; i = i + #all; @@ -101,10 +101,15 @@ local function parse_part(raw, i, allow_version) local comment_end; if raw:sub(i, i + 3) == "", i); + if comment_end then + i = comment_end + 3; + else + i = #raw + 1; + end - _, comment_end = raw:find("-->", i); - i = comment_end or #raw; i = skip_spaces(raw, i); end until not comment_end; @@ -127,7 +132,7 @@ local function parse_part(raw, i, allow_version) i = i + 2; i = skip_spaces(raw, i); - local tag = raw:match("[%w0-9%-]+", 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; @@ -145,7 +150,6 @@ local function parse_part(raw, i, allow_version) 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; @@ -161,7 +165,10 @@ local function parse_part(raw, i, allow_version) 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*$"; + local text_part = raw:sub(i, text_end and text_end - 1 or #raw) + :match "^%s*(.-)%s*$" + :gsub("%s+", " "); + if text_part ~= "" then text_parts[#text_parts + 1] = text_part; end @@ -175,8 +182,13 @@ local function parse_part(raw, i, allow_version) if raw:sub(i, i + 3) == "", i); - i = comment_end and comment_end + 1 or #raw; + comment_end = raw:find("-->", i); + if comment_end then + i = comment_end + 3; + else + i = #raw + 1; + end + i = skip_spaces(raw, i); else break @@ -210,9 +222,11 @@ local function parse(raw) first = false; + pprint(part); + if part.type == "text" then if #stack == 1 then - error "text may not appear outside a tag"; + error("text may not appear outside a tag (near '" .. raw:sub(i, i + 25) .. "')"); else curr_node[#curr_node + 1] = part.text; end diff --git a/src/init.lua b/src/init.lua index 774c540..18eff9e 100644 --- a/src/init.lua +++ b/src/init.lua @@ -4,13 +4,53 @@ local actions = { help = require "cli.help", query = require "cli.query", dump = require "cli.dump", + pack = require "cli.pack", }; -return function (action, ...) - if action == "--help" or action == "-h" or action == "-help" then - return actions.help(); +local function parse_args(...) + local options = {}; + local args = {}; + + require "util.args" ({ + function (arg) + if options.action then + args[#args + 1] = arg; + else + options.action = arg; + end + + return true, true; + end, + repo = function (arg) + if options.repo ~= nil then + error "a repo may be specified only once"; + else + options.repo = arg; + return true; + end + end, + help = actions.help, + }, ...); + + if not options.action then + error "no action specified"; end - return actions[action](...); + return options, args; +end + +return function (...) + xpcall( + function (...) + local options, args = parse_args(...); + options.repo = options.repo or "https://dl.flathub.org/repo"; + + return actions[options.action](options, table.unpack(args)); + end, + function (err) + print(table.concat { "unhandled error: ", err, "\n", debug.traceback(nil, 2):match "^[^\n]+\n(.+)$" }); + end, + ... + ); end diff --git a/src/ostree.lua b/src/ostree.lua index 5c574af..7d119a5 100644 --- a/src/ostree.lua +++ b/src/ostree.lua @@ -1,6 +1,7 @@ local gvariant = require "formats.gvariant"; local fmt = require "fmt"; local zlib = require "zlib"; +local fs = require "fs"; local header_fmt = fmt.new ">! I4 I4"; @@ -42,7 +43,7 @@ local read_header = gvariant "tuuuusa(ayay)"; --- - r: checksum --- - a{sv}: metadata --- - a{sv}: metadata -local read_summary = gvariant "a{s(tra{sv})}a{sv}"; +local read_summary = gvariant "l(s(tra{sv}))a{sv}"; --- Flatpak-specific format (I think): --- - a{s(rara{sv})}: map of architecture name to data about it @@ -52,6 +53,61 @@ local read_summary = gvariant "a{s(tra{sv})}a{sv}"; --- - a{sv}: metadata local read_summary_index = gvariant "a{s(rara{sv})}a{sv}"; +--- @class ostree_file +--- @field type "file" +--- @field uid integer +--- @field gid integer +--- @field mode integer +--- @field size integer +--- @field get_content fun(): fun(): string? + +--- @class ostree_link +--- @field type "link" +--- @field uid integer +--- @field gid integer +--- @field mode integer +--- @field target string + +--- @class ostree_dir +--- @field type "dir" +--- @field files { [string]: string } +--- @field dirs { [string]: { [1]: string, [2]: string } } +--- @field uid integer +--- @field gid integer +--- @field mode integer +local ostree_dir = {}; +ostree_dir.__index = ostree_dir; + +--- @param repo ostree +--- @param name string +function ostree_dir:file(repo, name) + if self.files[name] == nil then return nil end + return repo:read_file(self.files[name]); +end +--- @param repo ostree +--- @param name string +function ostree_dir:dir(repo, name) + if self.dirs[name] == nil then return nil end + return repo:read_dir(self.dirs[name]); +end +--- @param repo ostree +--- @param callback fun(path: string, val: ostree_dir | ostree_file | ostree_link) +function ostree_dir:recurse(repo, callback) + local function recurser(self, repo, callback, path) + callback(path, self); + + for file, ref in pairs(self.files) do + callback(path .. file, repo:read_file(ref)); + end + + for dir, ref in pairs(self.dirs) do + recurser(repo:read_dir(ref), repo, callback, path .. dir .. "/"); + end + end + + return recurser(self, repo, callback, "/"); +end + --- @class ostree --- @field reader fun(path: string): string local ostree = {}; @@ -119,14 +175,15 @@ function ostree.parse_dir(dir, meta) 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 }; + return setmetatable({ type = "dir", files = files, dirs = dirs, uid = uid, gid = gid, mode = mode & 0xFFF, xattrs = xattrs }, ostree_dir); else - return { type = "dir", files = files, dirs = dirs }; + return setmetatable({ type = "dir", files = files, dirs = dirs }, ostree_dir); end end --- @param data string +--- @return ostree_file function ostree.parse_file(data) - local header_size, idk = header_fmt:unpack(data); + local header_size = header_fmt:unpack(data); local header = data:sub(9, header_size + 8); local size, uid, gid, mode, rdev, link, xattrs = read_header(header); @@ -139,7 +196,7 @@ function ostree.parse_file(data) target = link, uid = uid, gid = gid, - mode = mode, + mode = mode & 0xFFF, xattrs = xattrs, }; end @@ -163,7 +220,7 @@ function ostree.parse_file(data) get_content = get_content, uid = uid, gid = gid, - mode = mode, + mode = mode & 0xFFF, size = size, xattrs = xattrs, }; @@ -173,16 +230,22 @@ end 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], - }; + local function refs_iter() + local el = refs(); + + if el == nil then + return nil; + else + return el[1], { + size = el[2][1], + checksum = el[2][2], + metadata = el[2][3], + }; + end end return { - refs = refs, + refs = refs_iter, metadata = metadata, }; end @@ -209,6 +272,31 @@ function ostree.parse_summary_index(data) metadata = metadata, }; end +--- @param path string +--- @param node ostree_dir | ostree_link | ostree_file +function ostree.dump_node(path, node) + if node.type == "dir" then + assert(fs.mkdir(path, true)); + assert(fs.chmod(path, node.mode & 0xFFF)); + elseif node.type == "file" then + assert(fs.mkdir(path:match "^(.+)/[^/]+$", true)); + + local f = assert(io.open(path, "w")); + + for seg in node.get_content() do + assert(f:write(seg)); + end + + assert(f:close()); + + assert(fs.chmod(path, node.mode & 0xFFF)); + elseif node.type == "link" then + assert(fs.mkdir(path:match "^(.+)/[^/]+$", true)); + assert(fs.symlink(path, node.target)); + else + error("Unknown node type '" .. node.type .. "'"); + end +end --- @param type string --- @param ref string @@ -223,9 +311,9 @@ 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]; + if type(dir) ~= "string" then meta = dir[2]; + dir = dir[1]; end return ostree.parse_dir(self:read_object("dirtree", dir), meta and self:read_object("dirmeta", meta)); @@ -235,6 +323,7 @@ function ostree:read_file(ref) return ostree.parse_file(self:read_object("filez", ref)); end function ostree:read_summary() + print(self.reader("summary")); return ostree.parse_summary(self.reader("summary")); end function ostree:read_subsummary(ref) diff --git a/src/util/args.lua b/src/util/args.lua index 74589aa..af558d6 100644 --- a/src/util/args.lua +++ b/src/util/args.lua @@ -1,4 +1,4 @@ ----@param consumers table +--- @param consumers table --- @param ... string --- @returns nil return function (consumers, ...) @@ -6,30 +6,30 @@ return function (consumers, ...) local pass_args = false; local function digest_arg(v) - local consumed = false; - while true do + --- @type (fun(arg: string): boolean?, boolean?)? 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; + return; end end - if not consumed then - if consumers[1](v) then + while true do + local res, fin = consumers[1](v); + if fin then pass_args = true; end + + if res or fin then + return; + end end end diff --git a/src/util/printing.lua b/src/util/printing.lua index cd65224..6668c71 100644 --- a/src/util/printing.lua +++ b/src/util/printing.lua @@ -1,12 +1,3 @@ -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); @@ -17,10 +8,6 @@ local function stringify_impl(obj, indent_str, n, passed) 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 @@ -53,7 +40,9 @@ local function stringify_impl(obj, indent_str, n, passed) end end - local meta = getmetatable(obj); + for i = 1, len do + parts[#parts + 1] = stringify_impl(obj[i], indent_str, n + 1, passed) .. ","; + end passed[obj] = false; @@ -76,7 +65,7 @@ local function stringify_impl(obj, indent_str, n, passed) return table.concat { "{", contents, "}" }; elseif kind == "string" then - return "\"" .. escape_str(obj) .. "\""; + return ("%q"):format(obj); elseif kind == "function" then local data = debug.getinfo(obj, "S"); return table.concat { tostring(obj), " @ ", data.short_src, ":", data.linedefined };