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
Requires `libsqlite3-dev`, `xdg-utils` (provides `xdg-open`), and `gcc` on PATH.
Requires `xdg-utils` (provides `xdg-open`) and `gcc` on PATH.
```sh
# Debian/Ubuntu
sudo apt install libsqlite3-dev xdg-utils gcc
sudo apt install xdg-utils gcc
# Fedora
sudo dnf install sqlite-devel xdg-utils gcc
sudo dnf install xdg-utils gcc
# 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.
@@ -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:
**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`.
+99 -67
View File
@@ -5,7 +5,7 @@
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sqlite3.h>
#include "sqlite_reader.h"
#include <libgen.h>
#include <ctype.h>
#include <time.h>
@@ -393,6 +393,70 @@ void scan_steam() {
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() {
char db_paths[8][MAX_PATH];
int num_dbs = 0;
@@ -474,80 +538,48 @@ void scan_lutris() {
if (!chosen_db) return;
sqlite3 *db;
int rc = sqlite3_open_v2(chosen_db, &db, SQLITE_OPEN_READONLY, NULL);
if (rc != SQLITE_OK) return;
SqliteDb db;
if (!sq_open(&db, chosen_db)) return;
sqlite3_stmt *stmt;
struct {
const char *name;
int require_installed;
int filter_installed;
} cands[] = {
{"games", 1, 1},
{"installed_game", 0, 0},
{"game", 1, 1},
{NULL, 0, 0}
};
const char *query =
"SELECT id, name, slug, runner "
"FROM games WHERE installed = 1";
SqliteTable t;
rc = sqlite3_prepare_v2(db, query, -1, &stmt, NULL);
for (int i = 0; cands[i].name; i++) {
memset(&t, 0, sizeof(t));
if (!sq_find_table(&db, cands[i].name, &t)) continue;
if (rc != SQLITE_OK) {
const char *alt_queries[] = {
"SELECT id, name, slug, runner FROM installed_game",
"SELECT id, name, slug, runner FROM game WHERE installed = 1",
NULL
};
int col_id = sq_column_index(&t, "id");
int col_name = sq_column_index(&t, "name");
int col_slug = sq_column_index(&t, "slug");
int col_runner = sq_column_index(&t, "runner");
if (col_id < 0 || col_name < 0 || col_slug < 0 || col_runner < 0) continue;
for (int q = 0; alt_queries[q]; q++) {
rc = sqlite3_prepare_v2(db, alt_queries[q], -1, &stmt, NULL);
if (rc == SQLITE_OK) break;
}
int col_installed = sq_column_index(&t, "installed");
if (cands[i].require_installed && col_installed < 0) continue;
if (rc != SQLITE_OK) {
sqlite3_close(db);
return;
}
LutrisCtx ctx;
ctx.t = &t;
ctx.col_id = col_id;
ctx.col_name = col_name;
ctx.col_slug = col_slug;
ctx.col_runner = col_runner;
ctx.col_installed = cands[i].filter_installed ? col_installed : -1;
sq_walk_table(&db, t.rootpage, lutris_row_cb, &ctx);
break;
}
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char *id = (const char *)sqlite3_column_text(stmt, 0);
const char *name = (const char *)sqlite3_column_text(stmt, 1);
const char *slug = (const char *)sqlite3_column_text(stmt, 2);
const char *runner = (const char *)sqlite3_column_text(stmt, 3);
if (!name || !slug) continue;
if (game_exists(name)) continue;
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 c = 0; cover_dirs[c]; c++) {
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;
}
}
if (cover[0]) break;
}
char run_command[MAX_RUN_CMD];
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);
sq_close(&db);
}
void scan_lutris_manual_files() {
+32 -4
View File
@@ -36,6 +36,31 @@ end
local binary = noctalia.pluginDir() .. "/gamelauncher"
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 protocols = { "steam://", "lutris:", "heroic://" }
@@ -362,15 +387,17 @@ local function buildBinary()
loading = true
errorMsg = ""
render()
local ok = noctalia.runAsync("cc -o " .. binary .. " " .. cSource .. " -lsqlite3", function(res)
local cmd = buildCommand()
local ok = noctalia.runAsync(cmd, function(res)
building = false
if res.exitCode == 0 then
loading = false
markBuilt()
scanGames()
else
loading = false
errorMsg = "Failed to build gamelauncher. Install gcc and libsqlite3-dev. (exit " .. (res.exitCode or "?") .. ")"
noctalia.log("build failed: " .. (res.stderr or "no stderr"))
errorMsg = "Failed to build gamelauncher. Install gcc. (exit " .. (res.exitCode or "?") .. ")"
noctalia.log("build failed (cmd: " .. cmd .. "): " .. (res.stderr or "no stderr"))
render()
end
end)
@@ -384,7 +411,7 @@ end
local function scanGames()
if loading or building then return end
if not noctalia.fileExists(binary) then
if not binaryReady() then
buildBinary()
return
end
@@ -404,6 +431,7 @@ local function scanGames()
local parsed, err2 = noctalia.json.decode(res.stdout)
if parsed then
games = parsed
pcall(noctalia.state.set, "games", parsed)
fetchMissingCovers()
else
errorMsg = "Failed to parse game list: " .. (err2 or "unknown error")
+2 -2
View File
@@ -1,12 +1,12 @@
id = "alexander/game-launcher"
name = "Game Launcher"
version = "1.1.2"
version = "1.3.0"
plugin_api = 13
author = "Alexander"
license = "MIT"
icon = "device-gamepad-2"
description = "Browse and launch games from Steam, Lutris, and Heroic."
dependencies = ["cc", "libsqlite3-dev", "xdg-utils"]
dependencies = ["cc", "xdg-utils"]
tags = ["gaming", "launcher", "utility"]
[[widget]]
+71 -56
View File
@@ -1,78 +1,92 @@
local games = {}
local ready = false
local binary = noctalia.pluginDir() .. "/gamelauncher"
local cSource = noctalia.pluginDir() .. "/gamelauncher.c"
local lastQuery = ""
local function statusRow(title, subtitle, glyph)
return { id = "", title = title, subtitle = subtitle, glyph = glyph }
end
local function isRunnerEnabled(runner)
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
end
local function getRunnerFlags()
local flags = ""
if isRunnerEnabled("steam") then flags = flags .. " --steam" end
if isRunnerEnabled("lutris") then flags = flags .. " --lutris" end
if isRunnerEnabled("heroic") then flags = flags .. " --heroic" end
return flags
local function loadGames()
local ok, v = pcall(noctalia.state.get, "games")
if ok and type(v) == "table" and #v > 0 then
games = v
return true
end
local base = noctalia.getenv("XDG_CACHE_HOME")
if not base or #base == 0 then
local home = noctalia.getenv("HOME") or ""
base = home .. "/.cache"
end
local raw = noctalia.readFile(base .. "/gamelauncher/games.json")
if raw then
local parsed = noctalia.json.decode(raw)
if type(parsed) == "table" and #parsed > 0 then
games = parsed
return true
end
end
return false
end
local function ensureGames(cb)
local function runScan()
local ok = noctalia.runAsync(binary .. getRunnerFlags() .. " --force", function(res)
if res.exitCode == 0 and res.stdout and #res.stdout > 0 then
local parsed, err = noctalia.json.decode(res.stdout)
if parsed then
games = parsed
ready = true
end
end
cb()
end)
if not ok then cb() end
local function filterRows(text)
local lower = noctalia.string.trim(text):lower()
local results = {}
for _, g in ipairs(games) do
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 m = meta[g.runner] or { glyph = "app-window", color = "on_surface" }
table.insert(results, {
id = g.id,
title = g.name,
subtitle = "Launch via " .. g.runner,
glyph = m.glyph,
})
end
end
if ready then
cb()
return
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
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()
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)
function onQuery(text)
lastQuery = text
if text == "" then
launcher.setResults(text, {
{ id = "hint", title = "Type a game name to search", glyph = "device-gamepad-2" },
statusRow("Type a game name to search", "Searches Steam, Lutris, and Heroic.", "device-gamepad-2"),
})
return
end
ensureGames(function()
local lower = text:lower()
local results = {}
for _, g in ipairs(games) do
if 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 m = meta[g.runner] or { glyph = "app-window", color = "on_surface" }
table.insert(results, {
id = g.id,
title = g.name,
subtitle = "Launch via " .. g.runner,
glyph = m.glyph,
})
end
end
launcher.setResults(text, results)
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
local function isValidProtocol(cmd)
@@ -87,6 +101,7 @@ local function isValidProtocol(cmd)
end
function onActivate(id)
if id == "" then return end
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
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