Files
community-plugins/game-launcher/sqlite_reader.c
T
b5098f8eed 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>
2026-08-02 23:12:29 -04:00

309 lines
10 KiB
C

#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;
}