Fix/gamelauncher nixos build (#225)
* game-launcher: bundle self-contained SQLite reader, drop libsqlite3 dependency Fixes building gamelauncher.c on NixOS where the runtime env lacks sqlite3.h/libsqlite3. scan_lutris now reads Lutris' pga.db through a bundled read-only SQLite file parser instead of linking sqlite3. * game-launcher: rework /g provider to reuse panel scan results The launcher provider now reads the games the panel already scanned via state/cache, trims the query, and surfaces status/error rows instead of duplicating the scan. --------- Co-authored-by: Ahmed5Emad <ahmed5emad@users.noreply.github.com>
This commit is contained in:
@@ -12,17 +12,17 @@ Browse and launch games from Steam, Lutris, and Heroic Games Launcher directly f
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
Requires `libsqlite3-dev`, `xdg-utils` (provides `xdg-open`), and `gcc` on PATH.
|
Requires `xdg-utils` (provides `xdg-open`) and `gcc` on PATH.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Debian/Ubuntu
|
# Debian/Ubuntu
|
||||||
sudo apt install libsqlite3-dev xdg-utils gcc
|
sudo apt install xdg-utils gcc
|
||||||
|
|
||||||
# Fedora
|
# Fedora
|
||||||
sudo dnf install sqlite-devel xdg-utils gcc
|
sudo dnf install xdg-utils gcc
|
||||||
|
|
||||||
# Arch
|
# Arch
|
||||||
sudo pacman -S sqlite xdg-utils gcc
|
sudo pacman -S xdg-utils gcc
|
||||||
```
|
```
|
||||||
|
|
||||||
The scanner binary (`gamelauncher`) is compiled automatically on first use — the plugin runs `cc` to build it when needed. No manual build step required.
|
The scanner binary (`gamelauncher`) is compiled automatically on first use — the plugin runs `cc` to build it when needed. No manual build step required.
|
||||||
@@ -55,7 +55,7 @@ From the launcher, type `/g` followed by a game name to search. Activate a resul
|
|||||||
|
|
||||||
The plugin addresses all findings from Noctalia's security audit:
|
The plugin addresses all findings from Noctalia's security audit:
|
||||||
|
|
||||||
**1. No shell commands in C scanner** — The scanner (`gamelauncher.c`) uses only local filesystem reads and SQLite queries. No `system()`, `popen()`, `curl`, `wget`, `python3`, or `grep` is invoked. All network requests (cover downloads) are handled in Luau via Noctalia's built-in `noctalia.http` and `noctalia.download` APIs, which respect offline mode.
|
**1. No shell commands in C scanner** — The scanner (`gamelauncher.c`) uses only local filesystem reads and a bundled SQLite database reader. No `system()`, `popen()`, `curl`, `wget`, `python3`, or `grep` is invoked. All network requests (cover downloads) are handled in Luau via Noctalia's built-in `noctalia.http` and `noctalia.download` APIs, which respect offline mode.
|
||||||
|
|
||||||
**2. No shell injection in launch paths** — The C scanner outputs protocol URLs only (e.g., `steam://rungameid/730`, `lutris:rungame/slug`, `heroic://launch/appid`). Luau validates each URL against known protocol prefixes, filters every character through a strict allowlist (`[%w_%-%.%/]` — no shell metacharacters), and double-quotes the argument before passing it to `xdg-open` via `noctalia.runAsync`.
|
**2. No shell injection in launch paths** — The C scanner outputs protocol URLs only (e.g., `steam://rungameid/730`, `lutris:rungame/slug`, `heroic://launch/appid`). Luau validates each URL against known protocol prefixes, filters every character through a strict allowlist (`[%w_%-%.%/]` — no shell metacharacters), and double-quotes the argument before passing it to `xdg-open` via `noctalia.runAsync`.
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <sqlite3.h>
|
#include "sqlite_reader.h"
|
||||||
#include <libgen.h>
|
#include <libgen.h>
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
@@ -393,6 +393,70 @@ void scan_steam() {
|
|||||||
free(steamapps_paths);
|
free(steamapps_paths);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
SqliteTable *t;
|
||||||
|
int col_id;
|
||||||
|
int col_name;
|
||||||
|
int col_slug;
|
||||||
|
int col_runner;
|
||||||
|
int col_installed;
|
||||||
|
} LutrisCtx;
|
||||||
|
|
||||||
|
static int lutris_row_cb(void *ctxp, long long rowid, const unsigned char *rec, int reclen) {
|
||||||
|
LutrisCtx *c = ctxp;
|
||||||
|
SqliteRecHeader h;
|
||||||
|
if (!sq_parse_header(rec, reclen, &h)) return 0;
|
||||||
|
|
||||||
|
char id[MAX_STR] = "0";
|
||||||
|
char name[MAX_STR] = "";
|
||||||
|
char slug[MAX_STR] = "";
|
||||||
|
char runner[MAX_STR] = "linux";
|
||||||
|
|
||||||
|
if (c->t->id_is_rowid)
|
||||||
|
snprintf(id, sizeof(id), "%lld", rowid);
|
||||||
|
else if (c->col_id >= 0)
|
||||||
|
sq_col_text(&h, rec, reclen, c->col_id, id, sizeof(id));
|
||||||
|
|
||||||
|
int has_name = sq_col_text(&h, rec, reclen, c->col_name, name, sizeof(name));
|
||||||
|
int has_slug = sq_col_text(&h, rec, reclen, c->col_slug, slug, sizeof(slug));
|
||||||
|
|
||||||
|
if (!has_name || !has_slug) return 0;
|
||||||
|
if (game_exists(name)) return 0;
|
||||||
|
|
||||||
|
int has_runner = sq_col_text(&h, rec, reclen, c->col_runner, runner, sizeof(runner));
|
||||||
|
if (!has_runner) strncpy(runner, "linux", sizeof(runner) - 1);
|
||||||
|
|
||||||
|
char cover[MAX_PATH] = "";
|
||||||
|
const char *cover_dirs[] = {
|
||||||
|
"/.local/share/lutris/coverart",
|
||||||
|
"/.var/app/net.lutris.Lutris/data/lutris/coverart",
|
||||||
|
"/.cache/lutris/coverart",
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int cd = 0; cover_dirs[cd]; cd++) {
|
||||||
|
char cover_dir[MAX_PATH];
|
||||||
|
snprintf(cover_dir, sizeof(cover_dir), "%s%s", get_home(), cover_dirs[cd]);
|
||||||
|
|
||||||
|
const char *exts[] = {".jpg", ".png", ".jpeg", NULL};
|
||||||
|
for (int e = 0; exts[e]; e++) {
|
||||||
|
char fp[MAX_PATH];
|
||||||
|
snprintf(fp, sizeof(fp), "%s/%s%s", cover_dir, slug, exts[e]);
|
||||||
|
if (file_exists(fp)) {
|
||||||
|
strncpy(cover, fp, MAX_PATH - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cover[0]) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
char run_command[MAX_RUN_CMD];
|
||||||
|
snprintf(run_command, sizeof(run_command), "lutris:rungame/%s", slug);
|
||||||
|
|
||||||
|
add_game(id, name, "lutris", cover, "", run_command, slug);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
void scan_lutris() {
|
void scan_lutris() {
|
||||||
char db_paths[8][MAX_PATH];
|
char db_paths[8][MAX_PATH];
|
||||||
int num_dbs = 0;
|
int num_dbs = 0;
|
||||||
@@ -474,80 +538,48 @@ void scan_lutris() {
|
|||||||
|
|
||||||
if (!chosen_db) return;
|
if (!chosen_db) return;
|
||||||
|
|
||||||
sqlite3 *db;
|
SqliteDb db;
|
||||||
int rc = sqlite3_open_v2(chosen_db, &db, SQLITE_OPEN_READONLY, NULL);
|
if (!sq_open(&db, chosen_db)) return;
|
||||||
if (rc != SQLITE_OK) return;
|
|
||||||
|
|
||||||
sqlite3_stmt *stmt;
|
struct {
|
||||||
|
const char *name;
|
||||||
const char *query =
|
int require_installed;
|
||||||
"SELECT id, name, slug, runner "
|
int filter_installed;
|
||||||
"FROM games WHERE installed = 1";
|
} cands[] = {
|
||||||
|
{"games", 1, 1},
|
||||||
rc = sqlite3_prepare_v2(db, query, -1, &stmt, NULL);
|
{"installed_game", 0, 0},
|
||||||
|
{"game", 1, 1},
|
||||||
if (rc != SQLITE_OK) {
|
{NULL, 0, 0}
|
||||||
const char *alt_queries[] = {
|
|
||||||
"SELECT id, name, slug, runner FROM installed_game",
|
|
||||||
"SELECT id, name, slug, runner FROM game WHERE installed = 1",
|
|
||||||
NULL
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (int q = 0; alt_queries[q]; q++) {
|
SqliteTable t;
|
||||||
rc = sqlite3_prepare_v2(db, alt_queries[q], -1, &stmt, NULL);
|
|
||||||
if (rc == SQLITE_OK) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rc != SQLITE_OK) {
|
for (int i = 0; cands[i].name; i++) {
|
||||||
sqlite3_close(db);
|
memset(&t, 0, sizeof(t));
|
||||||
return;
|
if (!sq_find_table(&db, cands[i].name, &t)) continue;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
int col_id = sq_column_index(&t, "id");
|
||||||
const char *id = (const char *)sqlite3_column_text(stmt, 0);
|
int col_name = sq_column_index(&t, "name");
|
||||||
const char *name = (const char *)sqlite3_column_text(stmt, 1);
|
int col_slug = sq_column_index(&t, "slug");
|
||||||
const char *slug = (const char *)sqlite3_column_text(stmt, 2);
|
int col_runner = sq_column_index(&t, "runner");
|
||||||
const char *runner = (const char *)sqlite3_column_text(stmt, 3);
|
if (col_id < 0 || col_name < 0 || col_slug < 0 || col_runner < 0) continue;
|
||||||
|
|
||||||
if (!name || !slug) continue;
|
int col_installed = sq_column_index(&t, "installed");
|
||||||
if (game_exists(name)) continue;
|
if (cands[i].require_installed && col_installed < 0) continue;
|
||||||
|
|
||||||
char cover[MAX_PATH] = "";
|
LutrisCtx ctx;
|
||||||
const char *cover_dirs[] = {
|
ctx.t = &t;
|
||||||
"/.local/share/lutris/coverart",
|
ctx.col_id = col_id;
|
||||||
"/.var/app/net.lutris.Lutris/data/lutris/coverart",
|
ctx.col_name = col_name;
|
||||||
"/.cache/lutris/coverart",
|
ctx.col_slug = col_slug;
|
||||||
NULL
|
ctx.col_runner = col_runner;
|
||||||
};
|
ctx.col_installed = cands[i].filter_installed ? col_installed : -1;
|
||||||
|
|
||||||
for (int c = 0; cover_dirs[c]; c++) {
|
sq_walk_table(&db, t.rootpage, lutris_row_cb, &ctx);
|
||||||
char cd[MAX_PATH];
|
|
||||||
snprintf(cd, sizeof(cd), "%s%s", get_home(), cover_dirs[c]);
|
|
||||||
|
|
||||||
const char *exts[] = {".jpg", ".png", ".jpeg", NULL};
|
|
||||||
for (int e = 0; exts[e]; e++) {
|
|
||||||
char fp[MAX_PATH];
|
|
||||||
snprintf(fp, sizeof(fp), "%s/%s%s", cd, slug, exts[e]);
|
|
||||||
if (file_exists(fp)) {
|
|
||||||
strncpy(cover, fp, MAX_PATH - 1);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (cover[0]) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
char run_command[MAX_RUN_CMD];
|
sq_close(&db);
|
||||||
snprintf(run_command, sizeof(run_command), "lutris:rungame/%s", slug);
|
|
||||||
|
|
||||||
if (!id) id = "0";
|
|
||||||
if (!runner) runner = "linux";
|
|
||||||
|
|
||||||
add_game(id, name, "lutris", cover, "", run_command, slug ? slug : "");
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlite3_finalize(stmt);
|
|
||||||
sqlite3_close(db);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void scan_lutris_manual_files() {
|
void scan_lutris_manual_files() {
|
||||||
|
|||||||
@@ -36,6 +36,31 @@ end
|
|||||||
|
|
||||||
local binary = noctalia.pluginDir() .. "/gamelauncher"
|
local binary = noctalia.pluginDir() .. "/gamelauncher"
|
||||||
local cSource = noctalia.pluginDir() .. "/gamelauncher.c"
|
local cSource = noctalia.pluginDir() .. "/gamelauncher.c"
|
||||||
|
local cReaderSource = noctalia.pluginDir() .. "/sqlite_reader.c"
|
||||||
|
|
||||||
|
local function buildCommand()
|
||||||
|
return "cc -o " .. binary .. " " .. cSource .. " " .. cReaderSource
|
||||||
|
end
|
||||||
|
|
||||||
|
local buildTagFile
|
||||||
|
do
|
||||||
|
local ok, dir = pcall(noctalia.pluginDataDir)
|
||||||
|
if ok and dir then buildTagFile = dir .. "/build_command" end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function binaryReady()
|
||||||
|
if not noctalia.fileExists(binary) then return false end
|
||||||
|
if not buildTagFile then return true end
|
||||||
|
if noctalia.readFile(buildTagFile) ~= buildCommand() then
|
||||||
|
noctalia.removeFile(binary)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function markBuilt()
|
||||||
|
if buildTagFile then noctalia.writeFile(buildTagFile, buildCommand()) end
|
||||||
|
end
|
||||||
|
|
||||||
local function isValidProtocol(cmd)
|
local function isValidProtocol(cmd)
|
||||||
local protocols = { "steam://", "lutris:", "heroic://" }
|
local protocols = { "steam://", "lutris:", "heroic://" }
|
||||||
@@ -362,15 +387,17 @@ local function buildBinary()
|
|||||||
loading = true
|
loading = true
|
||||||
errorMsg = ""
|
errorMsg = ""
|
||||||
render()
|
render()
|
||||||
local ok = noctalia.runAsync("cc -o " .. binary .. " " .. cSource .. " -lsqlite3", function(res)
|
local cmd = buildCommand()
|
||||||
|
local ok = noctalia.runAsync(cmd, function(res)
|
||||||
building = false
|
building = false
|
||||||
if res.exitCode == 0 then
|
if res.exitCode == 0 then
|
||||||
loading = false
|
loading = false
|
||||||
|
markBuilt()
|
||||||
scanGames()
|
scanGames()
|
||||||
else
|
else
|
||||||
loading = false
|
loading = false
|
||||||
errorMsg = "Failed to build gamelauncher. Install gcc and libsqlite3-dev. (exit " .. (res.exitCode or "?") .. ")"
|
errorMsg = "Failed to build gamelauncher. Install gcc. (exit " .. (res.exitCode or "?") .. ")"
|
||||||
noctalia.log("build failed: " .. (res.stderr or "no stderr"))
|
noctalia.log("build failed (cmd: " .. cmd .. "): " .. (res.stderr or "no stderr"))
|
||||||
render()
|
render()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
@@ -384,7 +411,7 @@ end
|
|||||||
|
|
||||||
local function scanGames()
|
local function scanGames()
|
||||||
if loading or building then return end
|
if loading or building then return end
|
||||||
if not noctalia.fileExists(binary) then
|
if not binaryReady() then
|
||||||
buildBinary()
|
buildBinary()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -404,6 +431,7 @@ local function scanGames()
|
|||||||
local parsed, err2 = noctalia.json.decode(res.stdout)
|
local parsed, err2 = noctalia.json.decode(res.stdout)
|
||||||
if parsed then
|
if parsed then
|
||||||
games = parsed
|
games = parsed
|
||||||
|
pcall(noctalia.state.set, "games", parsed)
|
||||||
fetchMissingCovers()
|
fetchMissingCovers()
|
||||||
else
|
else
|
||||||
errorMsg = "Failed to parse game list: " .. (err2 or "unknown error")
|
errorMsg = "Failed to parse game list: " .. (err2 or "unknown error")
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
id = "alexander/game-launcher"
|
id = "alexander/game-launcher"
|
||||||
name = "Game Launcher"
|
name = "Game Launcher"
|
||||||
version = "1.1.2"
|
version = "1.3.0"
|
||||||
plugin_api = 13
|
plugin_api = 13
|
||||||
author = "Alexander"
|
author = "Alexander"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
icon = "device-gamepad-2"
|
icon = "device-gamepad-2"
|
||||||
description = "Browse and launch games from Steam, Lutris, and Heroic."
|
description = "Browse and launch games from Steam, Lutris, and Heroic."
|
||||||
dependencies = ["cc", "libsqlite3-dev", "xdg-utils"]
|
dependencies = ["cc", "xdg-utils"]
|
||||||
tags = ["gaming", "launcher", "utility"]
|
tags = ["gaming", "launcher", "utility"]
|
||||||
|
|
||||||
[[widget]]
|
[[widget]]
|
||||||
|
|||||||
+65
-50
@@ -1,66 +1,43 @@
|
|||||||
local games = {}
|
local games = {}
|
||||||
local ready = false
|
local lastQuery = ""
|
||||||
local binary = noctalia.pluginDir() .. "/gamelauncher"
|
|
||||||
local cSource = noctalia.pluginDir() .. "/gamelauncher.c"
|
local function statusRow(title, subtitle, glyph)
|
||||||
|
return { id = "", title = title, subtitle = subtitle, glyph = glyph }
|
||||||
|
end
|
||||||
|
|
||||||
local function isRunnerEnabled(runner)
|
local function isRunnerEnabled(runner)
|
||||||
local ok, val = pcall(noctalia.getConfig, runner .. "_enabled")
|
local ok, val = pcall(noctalia.getConfig, runner .. "_enabled")
|
||||||
if ok then return val == true or val == "true" end
|
if ok and val ~= nil then return val == true or val == "true" end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
local function getRunnerFlags()
|
local function loadGames()
|
||||||
local flags = ""
|
local ok, v = pcall(noctalia.state.get, "games")
|
||||||
if isRunnerEnabled("steam") then flags = flags .. " --steam" end
|
if ok and type(v) == "table" and #v > 0 then
|
||||||
if isRunnerEnabled("lutris") then flags = flags .. " --lutris" end
|
games = v
|
||||||
if isRunnerEnabled("heroic") then flags = flags .. " --heroic" end
|
return true
|
||||||
return flags
|
end
|
||||||
end
|
local base = noctalia.getenv("XDG_CACHE_HOME")
|
||||||
|
if not base or #base == 0 then
|
||||||
local function ensureGames(cb)
|
local home = noctalia.getenv("HOME") or ""
|
||||||
local function runScan()
|
base = home .. "/.cache"
|
||||||
local ok = noctalia.runAsync(binary .. getRunnerFlags() .. " --force", function(res)
|
end
|
||||||
if res.exitCode == 0 and res.stdout and #res.stdout > 0 then
|
local raw = noctalia.readFile(base .. "/gamelauncher/games.json")
|
||||||
local parsed, err = noctalia.json.decode(res.stdout)
|
if raw then
|
||||||
if parsed then
|
local parsed = noctalia.json.decode(raw)
|
||||||
|
if type(parsed) == "table" and #parsed > 0 then
|
||||||
games = parsed
|
games = parsed
|
||||||
ready = true
|
return true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
cb()
|
return false
|
||||||
end)
|
|
||||||
if not ok then cb() end
|
|
||||||
end
|
|
||||||
if ready then
|
|
||||||
cb()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if not noctalia.fileExists(binary) then
|
|
||||||
local ok = noctalia.runAsync("cc -o " .. binary .. " " .. cSource .. " -lsqlite3", function(res)
|
|
||||||
if res.exitCode == 0 then
|
|
||||||
runScan()
|
|
||||||
else
|
|
||||||
cb()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
if not ok then cb() end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
runScan()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function onQuery(text)
|
local function filterRows(text)
|
||||||
if text == "" then
|
local lower = noctalia.string.trim(text):lower()
|
||||||
launcher.setResults(text, {
|
|
||||||
{ id = "hint", title = "Type a game name to search", glyph = "device-gamepad-2" },
|
|
||||||
})
|
|
||||||
return
|
|
||||||
end
|
|
||||||
ensureGames(function()
|
|
||||||
local lower = text:lower()
|
|
||||||
local results = {}
|
local results = {}
|
||||||
for _, g in ipairs(games) do
|
for _, g in ipairs(games) do
|
||||||
if g.name:lower():find(lower, 1, true) and isRunnerEnabled(g.runner) then
|
if g.name and g.runner and g.name:lower():find(lower, 1, true) and isRunnerEnabled(g.runner) then
|
||||||
local meta = { steam = { glyph = "brand-steam", color = "steam" }, lutris = { glyph = "device-gamepad", color = "warning" }, heroic = { glyph = "app-window", color = "heroic" } }
|
local meta = { steam = { glyph = "brand-steam", color = "steam" }, lutris = { glyph = "device-gamepad", color = "warning" }, heroic = { glyph = "app-window", color = "heroic" } }
|
||||||
local m = meta[g.runner] or { glyph = "app-window", color = "on_surface" }
|
local m = meta[g.runner] or { glyph = "app-window", color = "on_surface" }
|
||||||
table.insert(results, {
|
table.insert(results, {
|
||||||
@@ -71,8 +48,45 @@ function onQuery(text)
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
launcher.setResults(text, results)
|
if #results == 0 then
|
||||||
|
if #games == 0 then
|
||||||
|
return {
|
||||||
|
statusRow("No games found", "Open the Game Launcher panel to scan your games first.", "app-window"),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
statusRow("No games found", "Nothing matches \"" .. text .. "\". Try a different name.", "app-window"),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
return results
|
||||||
|
end
|
||||||
|
|
||||||
|
pcall(function()
|
||||||
|
noctalia.state.watch("games", function(v)
|
||||||
|
if type(v) == "table" then
|
||||||
|
games = v
|
||||||
|
if lastQuery ~= "" then
|
||||||
|
launcher.setResults(lastQuery, filterRows(lastQuery))
|
||||||
|
end
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
function onQuery(text)
|
||||||
|
lastQuery = text
|
||||||
|
if text == "" then
|
||||||
|
launcher.setResults(text, {
|
||||||
|
statusRow("Type a game name to search", "Searches Steam, Lutris, and Heroic.", "device-gamepad-2"),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not loadGames() then
|
||||||
|
launcher.setResults(text, {
|
||||||
|
statusRow("No games loaded", "Open the Game Launcher panel first so it can scan your games.", "device-gamepad-2"),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
launcher.setResults(text, filterRows(text))
|
||||||
end
|
end
|
||||||
|
|
||||||
local function isValidProtocol(cmd)
|
local function isValidProtocol(cmd)
|
||||||
@@ -87,6 +101,7 @@ local function isValidProtocol(cmd)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function onActivate(id)
|
function onActivate(id)
|
||||||
|
if id == "" then return end
|
||||||
for _, g in ipairs(games) do
|
for _, g in ipairs(games) do
|
||||||
if g.id == id and g.run_command and #g.run_command > 0 and isValidProtocol(g.run_command) then
|
if g.id == id and g.run_command and #g.run_command > 0 and isValidProtocol(g.run_command) then
|
||||||
noctalia.runAsync('xdg-open "' .. g.run_command .. '"')
|
noctalia.runAsync('xdg-open "' .. g.run_command .. '"')
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include "sqlite_reader.h"
|
||||||
|
|
||||||
|
static int sq_get16(const unsigned char *p) { return (p[0] << 8) | p[1]; }
|
||||||
|
static int sq_get32(const unsigned char *p) { return ((p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]); }
|
||||||
|
|
||||||
|
static long long sq_varint(const unsigned char *p, int *len) {
|
||||||
|
long long v = 0;
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < 8; i++) {
|
||||||
|
v = (v << 7) | (p[i] & 0x7f);
|
||||||
|
if (!(p[i] & 0x80)) { i++; break; }
|
||||||
|
}
|
||||||
|
if (i == 8) { v = (v << 8) | p[8]; i = 9; }
|
||||||
|
*len = i;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sq_open(SqliteDb *db, const char *path) {
|
||||||
|
memset(db, 0, sizeof(*db));
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f) return 0;
|
||||||
|
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return 0; }
|
||||||
|
long sz = ftell(f);
|
||||||
|
rewind(f);
|
||||||
|
if (sz < 100) { fclose(f); return 0; }
|
||||||
|
db->data = malloc((size_t)sz);
|
||||||
|
if (!db->data) { fclose(f); return 0; }
|
||||||
|
if (fread(db->data, 1, (size_t)sz, f) != (size_t)sz) { free(db->data); db->data = NULL; fclose(f); return 0; }
|
||||||
|
fclose(f);
|
||||||
|
db->size = (size_t)sz;
|
||||||
|
if (memcmp(db->data, "SQLite format 3", 16) != 0) { free(db->data); db->data = NULL; return 0; }
|
||||||
|
int ps = sq_get16(db->data + 16);
|
||||||
|
db->page_size = (ps == 1) ? 65536 : ps;
|
||||||
|
db->reserved = db->data[20];
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sq_close(SqliteDb *db) {
|
||||||
|
if (db->data) { free(db->data); db->data = NULL; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t sq_page_base(SqliteDb *db, int page) {
|
||||||
|
return (size_t)(page - 1) * (size_t)db->page_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t sq_btree_off(SqliteDb *db, int page) {
|
||||||
|
return sq_page_base(db, page) + (page == 1 ? 100 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sq_serial_size(long long st) {
|
||||||
|
switch (st) {
|
||||||
|
case 0: return 0;
|
||||||
|
case 1: return 1;
|
||||||
|
case 2: return 2;
|
||||||
|
case 3: return 3;
|
||||||
|
case 4: return 4;
|
||||||
|
case 5: return 6;
|
||||||
|
case 6: return 8;
|
||||||
|
case 7: return 8;
|
||||||
|
case 8: return 0;
|
||||||
|
case 9: return 0;
|
||||||
|
default: return (int)((st - 12) / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int sq_parse_header(const unsigned char *rec, int reclen, SqliteRecHeader *h) {
|
||||||
|
if (reclen < 1) return 0;
|
||||||
|
int vl;
|
||||||
|
long long hlen = sq_varint(rec, &vl);
|
||||||
|
if (hlen < 1 || hlen > reclen) return 0;
|
||||||
|
int pos = vl, i = 0;
|
||||||
|
while (pos < hlen && i < SQ_MAX_COLS) {
|
||||||
|
h->serial[i++] = sq_varint(rec + pos, &vl);
|
||||||
|
pos += vl;
|
||||||
|
}
|
||||||
|
if (pos != hlen) return 0;
|
||||||
|
h->n = i;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sq_col_text(const SqliteRecHeader *h, const unsigned char *rec, int reclen, int col, char *out, int outsz) {
|
||||||
|
out[0] = 0;
|
||||||
|
if (col < 0 || col >= h->n) return 0;
|
||||||
|
long long st = h->serial[col];
|
||||||
|
if (st == 0) return 0;
|
||||||
|
int vl;
|
||||||
|
long long hlen = sq_varint(rec, &vl);
|
||||||
|
int off = (int)hlen;
|
||||||
|
for (int j = 0; j < col; j++) off += sq_serial_size(h->serial[j]);
|
||||||
|
int sz = sq_serial_size(st);
|
||||||
|
if (off + sz > reclen) return 0;
|
||||||
|
const unsigned char *p = rec + off;
|
||||||
|
if (st >= 12) {
|
||||||
|
int n = sz;
|
||||||
|
if (n > outsz - 1) n = outsz - 1;
|
||||||
|
memcpy(out, p, (size_t)n);
|
||||||
|
out[n] = 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (st >= 1 && st <= 6) {
|
||||||
|
long long v = 0;
|
||||||
|
for (int i = 0; i < sz; i++) v = (v << 8) | p[i];
|
||||||
|
int bits = sz * 8;
|
||||||
|
if (bits < 64) { long long sign = 1LL << (bits - 1); if (v & sign) v -= (1LL << bits); }
|
||||||
|
snprintf(out, (size_t)outsz, "%lld", v);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (st == 7) { snprintf(out, (size_t)outsz, "%g", (double)*((double*)p)); return 1; }
|
||||||
|
if (st == 8) { snprintf(out, (size_t)outsz, "0"); return 1; }
|
||||||
|
if (st == 9) { snprintf(out, (size_t)outsz, "1"); return 1; }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
long long sq_col_int(const SqliteRecHeader *h, const unsigned char *rec, int reclen, int col) {
|
||||||
|
char buf[128];
|
||||||
|
if (!sq_col_text(h, rec, reclen, col, buf, sizeof(buf))) return 0;
|
||||||
|
return atoll(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sq_get_payload(SqliteDb *db, const unsigned char *ppos, long long plen, unsigned char *out) {
|
||||||
|
int usable = db->page_size - db->reserved;
|
||||||
|
int X = usable - 35;
|
||||||
|
int M = ((usable - 12) * 32 / 255) - 23;
|
||||||
|
int local;
|
||||||
|
if (plen <= X) local = (int)plen;
|
||||||
|
else {
|
||||||
|
local = M + (int)((plen - M) % (usable - 4));
|
||||||
|
if (local > X) local = X;
|
||||||
|
}
|
||||||
|
long long got = 0;
|
||||||
|
if (local > 0) { memcpy(out, ppos, (size_t)local); got = local; }
|
||||||
|
int next = 0;
|
||||||
|
if (got < plen) next = sq_get32(ppos + local);
|
||||||
|
int guard = 0;
|
||||||
|
while (got < plen && next && guard++ < 100000) {
|
||||||
|
size_t off = (size_t)(next - 1) * (size_t)db->page_size;
|
||||||
|
if (off + 4 > db->size) return 0;
|
||||||
|
const unsigned char *p = db->data + off;
|
||||||
|
int chunk = usable - 4;
|
||||||
|
long long rem = plen - got;
|
||||||
|
int n = (rem < chunk) ? (int)rem : chunk;
|
||||||
|
memcpy(out + got, p + 4, (size_t)n);
|
||||||
|
got += n;
|
||||||
|
next = sq_get32(p);
|
||||||
|
}
|
||||||
|
return got == plen;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sq_walk_page(SqliteDb *db, int page, sq_row_cb cb, void *ctx, int depth) {
|
||||||
|
if (depth > 64) return 0;
|
||||||
|
size_t boff = sq_btree_off(db, page);
|
||||||
|
if (boff + 8 > db->size) return 0;
|
||||||
|
const unsigned char *bh = db->data + boff;
|
||||||
|
int type = bh[0];
|
||||||
|
int ncell = sq_get16(bh + 3);
|
||||||
|
size_t base = sq_page_base(db, page);
|
||||||
|
if (type == 5) {
|
||||||
|
int right = sq_get32(bh + 8);
|
||||||
|
for (int i = 0; i < ncell; i++) {
|
||||||
|
if (boff + 12 + i * 2 + 2 > db->size) return 0;
|
||||||
|
int coff = sq_get16(db->data + boff + 12 + i * 2);
|
||||||
|
const unsigned char *cell = db->data + base + coff;
|
||||||
|
int child = sq_get32(cell);
|
||||||
|
if (!sq_walk_page(db, child, cb, ctx, depth + 1)) return 0;
|
||||||
|
}
|
||||||
|
if (right && !sq_walk_page(db, right, cb, ctx, depth + 1)) return 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (type == 13) {
|
||||||
|
for (int i = 0; i < ncell; i++) {
|
||||||
|
if (boff + 8 + i * 2 + 2 > db->size) return 0;
|
||||||
|
int coff = sq_get16(db->data + boff + 8 + i * 2);
|
||||||
|
const unsigned char *cell = db->data + base + coff;
|
||||||
|
int vl = 0, vl2 = 0;
|
||||||
|
long long plen = sq_varint(cell, &vl);
|
||||||
|
long long rowid = sq_varint(cell + vl, &vl2);
|
||||||
|
int hdr = vl + vl2;
|
||||||
|
if (plen < 0 || plen > (long long)db->size) return 0;
|
||||||
|
unsigned char *rec = malloc((size_t)plen + 1);
|
||||||
|
if (!rec) return 0;
|
||||||
|
int ok = sq_get_payload(db, cell + hdr, plen, rec);
|
||||||
|
rec[plen] = 0;
|
||||||
|
if (!ok) { free(rec); return 0; }
|
||||||
|
int r = cb(ctx, rowid, rec, (int)plen);
|
||||||
|
free(rec);
|
||||||
|
if (r) return 1;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sq_walk_table(SqliteDb *db, int rootpage, sq_row_cb cb, void *ctx) {
|
||||||
|
return sq_walk_page(db, rootpage, cb, ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sq_is_kw(const char *w) {
|
||||||
|
static const char *kws[] = {"PRIMARY", "UNIQUE", "CHECK", "FOREIGN", "CONSTRAINT", "CONSTRAINTS", NULL};
|
||||||
|
for (int i = 0; kws[i]; i++) if (strcasecmp(w, kws[i]) == 0) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sq_add_col(SqliteTable *t, const char *start, const char *end) {
|
||||||
|
while (start < end && isspace((unsigned char)*start)) start++;
|
||||||
|
while (end > start && isspace((unsigned char)end[-1])) end--;
|
||||||
|
if (start >= end || t->ncols >= SQ_MAX_COLS) return;
|
||||||
|
char w[128];
|
||||||
|
int n = 0;
|
||||||
|
const char *p = start;
|
||||||
|
if (*p == '"' || *p == '`' || *p == '[') {
|
||||||
|
char q = *p;
|
||||||
|
char close = (q == '[') ? ']' : q;
|
||||||
|
p++;
|
||||||
|
while (p < end && n < 127) {
|
||||||
|
if (*p == close) break;
|
||||||
|
w[n++] = *p++;
|
||||||
|
}
|
||||||
|
w[n] = 0;
|
||||||
|
if (n) { memcpy(t->colnames[t->ncols], w, (size_t)n + 1); t->ncols++; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (p < end && n < 127 && !isspace((unsigned char)*p) && *p != '(' && *p != ',') w[n++] = *p++;
|
||||||
|
w[n] = 0;
|
||||||
|
if (n == 0) return;
|
||||||
|
if (sq_is_kw(w)) return;
|
||||||
|
memcpy(t->colnames[t->ncols], w, (size_t)n + 1);
|
||||||
|
t->ncols++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int sq_parse_create(SqliteTable *t, const char *sql) {
|
||||||
|
t->ncols = 0;
|
||||||
|
const char *p = sql;
|
||||||
|
char q = 0;
|
||||||
|
int depth = 0;
|
||||||
|
const char *open = NULL;
|
||||||
|
for (; *p; p++) {
|
||||||
|
char c = *p;
|
||||||
|
if (q) { if (c == q) q = 0; continue; }
|
||||||
|
if (c == '\'' || c == '"' || c == '`') { q = c; continue; }
|
||||||
|
if (c == '(') { if (depth == 0 && !open) open = p; depth++; continue; }
|
||||||
|
if (c == ')') { if (depth > 0) depth--; if (depth == 0 && open) break; }
|
||||||
|
}
|
||||||
|
if (!open) return 0;
|
||||||
|
const char *end = p;
|
||||||
|
int d = 0;
|
||||||
|
char qq = 0;
|
||||||
|
const char *seg = open + 1;
|
||||||
|
for (const char *s = open + 1; s < end; s++) {
|
||||||
|
char c = *s;
|
||||||
|
if (qq) { if (c == qq) qq = 0; continue; }
|
||||||
|
if (c == '\'' || c == '"' || c == '`') { qq = c; continue; }
|
||||||
|
if (c == '(') { d++; continue; }
|
||||||
|
if (c == ')') { if (d > 0) d--; continue; }
|
||||||
|
if (c == ',' && d == 0) { sq_add_col(t, seg, s); seg = s + 1; }
|
||||||
|
}
|
||||||
|
sq_add_col(t, seg, end);
|
||||||
|
for (int i = 0; i < t->ncols; i++) {
|
||||||
|
if (strcmp(t->colnames[i], "id") == 0 && strcasestr(sql, "INTEGER PRIMARY KEY")) {
|
||||||
|
t->id_is_rowid = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t->ncols > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
SqliteDb *db;
|
||||||
|
const char *name;
|
||||||
|
SqliteTable *out;
|
||||||
|
int found;
|
||||||
|
} FindTableCtx;
|
||||||
|
|
||||||
|
static int find_table_cb(void *ctxp, long long rowid, const unsigned char *rec, int reclen) {
|
||||||
|
(void)rowid;
|
||||||
|
FindTableCtx *c = ctxp;
|
||||||
|
SqliteRecHeader h;
|
||||||
|
if (!sq_parse_header(rec, reclen, &h)) return 0;
|
||||||
|
char type[32], name[128];
|
||||||
|
if (!sq_col_text(&h, rec, reclen, 0, type, sizeof(type))) return 0;
|
||||||
|
if (strcmp(type, "table") != 0) return 0;
|
||||||
|
if (!sq_col_text(&h, rec, reclen, 1, name, sizeof(name))) return 0;
|
||||||
|
if (strcmp(name, c->name) != 0) return 0;
|
||||||
|
long long rp = sq_col_int(&h, rec, reclen, 3);
|
||||||
|
char sql[8192];
|
||||||
|
if (!sq_col_text(&h, rec, reclen, 4, sql, sizeof(sql))) return 0;
|
||||||
|
c->out->rootpage = (int)rp;
|
||||||
|
sq_parse_create(c->out, sql);
|
||||||
|
c->found = 1;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sq_find_table(SqliteDb *db, const char *name, SqliteTable *out) {
|
||||||
|
FindTableCtx ctx = { db, name, out, 0 };
|
||||||
|
sq_walk_page(db, 1, find_table_cb, &ctx, 0);
|
||||||
|
return ctx.found;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sq_column_index(const SqliteTable *t, const char *name) {
|
||||||
|
for (int i = 0; i < t->ncols; i++) {
|
||||||
|
if (strcmp(t->colnames[i], name) == 0) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#ifndef GAME_LAUNCHER_SQLITE_READER_H
|
||||||
|
#define GAME_LAUNCHER_SQLITE_READER_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#define SQ_MAX_COLS 64
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
unsigned char *data;
|
||||||
|
size_t size;
|
||||||
|
int page_size;
|
||||||
|
int reserved;
|
||||||
|
} SqliteDb;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int rootpage;
|
||||||
|
int ncols;
|
||||||
|
char colnames[SQ_MAX_COLS][64];
|
||||||
|
int id_is_rowid;
|
||||||
|
} SqliteTable;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
long long serial[SQ_MAX_COLS];
|
||||||
|
int n;
|
||||||
|
} SqliteRecHeader;
|
||||||
|
|
||||||
|
int sq_open(SqliteDb *db, const char *path);
|
||||||
|
void sq_close(SqliteDb *db);
|
||||||
|
|
||||||
|
int sq_find_table(SqliteDb *db, const char *name, SqliteTable *out);
|
||||||
|
int sq_column_index(const SqliteTable *t, const char *name);
|
||||||
|
|
||||||
|
typedef int (*sq_row_cb)(void *ctx, long long rowid, const unsigned char *rec, int reclen);
|
||||||
|
int sq_walk_table(SqliteDb *db, int rootpage, sq_row_cb cb, void *ctx);
|
||||||
|
|
||||||
|
int sq_parse_header(const unsigned char *rec, int reclen, SqliteRecHeader *h);
|
||||||
|
int sq_col_text(const SqliteRecHeader *h, const unsigned char *rec, int reclen, int col, char *out, int outsz);
|
||||||
|
long long sq_col_int(const SqliteRecHeader *h, const unsigned char *rec, int reclen, int col);
|
||||||
|
|
||||||
|
#endif
|
||||||
Reference in New Issue
Block a user