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:
Ahmed Emad
2026-08-02 23:12:29 -04:00
committed by GitHub
co-authored by Ahmed5Emad
parent cf1bda889a
commit b5098f8eed
7 changed files with 557 additions and 134 deletions
+5 -5
View File
@@ -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`.
+96 -64
View File
@@ -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() {
+32 -4
View File
@@ -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")
+2 -2
View File
@@ -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
View File
@@ -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 .. '"')
+308
View File
@@ -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;
}
+40
View File
@@ -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