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
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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 .. '"')
|
||||
|
||||
@@ -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