Files
Ustagammon/engine/compat/glib-shim.c
Pierre MARTINandClaude Opus 5.5 42bfb6506d Couche de compatibilité : shim GLib et en-têtes réduits
engine/compat/ remplace ce que le cœur de gnubg attend du reste de gnubg
et de GLib : config.h écrit à la main (sans threads, C générique, NEON
prévu), shim GLib minimal (dont un analyseur XML pour les tables d'équité
et mmap pour les bases de fin de partie), en-têtes réduits et fonctions de
support.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 13:56:29 +01:00

659 lines
16 KiB
C

/*
* glib-shim.c -- implémentation du shim minimal de GLib (voir glib-shim/glib.h).
*
* Copyright (C) 2026 les auteurs d'Ustagammon.
* Licence : GPLv3 ou ultérieure (voir LICENSE).
*
* Écrit pour Ustagammon d'après la documentation publique de GLib ; aucun code
* de GLib n'est repris.
*/
#define _DEFAULT_SOURCE
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <locale.h>
#include <stdarg.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include "glib.h"
/* ---------------------------------------------------------------- Erreurs */
void
ustagammon_g_assert_failed(const char *expr, const char *file, int line)
{
fprintf(stderr, "moteur : assertion « %s » fausse (%s:%d)\n", expr, file, line);
abort();
}
/* Crée une GError dont le message est « message : detail ». */
static GError *
new_error(gint code, const char *message, const char *detail)
{
GError *e = g_new0(GError, 1);
size_t size = strlen(message) + strlen(detail) + 4;
e->code = code;
e->message = g_malloc(size);
snprintf(e->message, size, "%s : %s", message, detail);
return e;
}
static void
set_error(GError **error, gint code, const char *message, const char *detail)
{
if (error)
*error = new_error(code, message, detail);
}
void
g_error_free(GError *error)
{
if (!error)
return;
g_free(error->message);
g_free(error);
}
/* ---------------------------------------------------------------- Mémoire */
static gpointer
check_allocation(gpointer p, gsize n)
{
if (!p && n) {
fprintf(stderr, "moteur : échec d'allocation de %zu octets\n", n);
abort();
}
return p;
}
gpointer
g_malloc(gsize n)
{
return n ? check_allocation(malloc(n), n) : NULL;
}
gpointer
g_malloc0(gsize n)
{
return n ? check_allocation(calloc(1, n), n) : NULL;
}
void
g_free(gpointer p)
{
free(p);
}
gpointer
g_memdup2(gconstpointer mem, gsize n)
{
gpointer p;
if (!mem || !n)
return NULL;
p = g_malloc(n);
memcpy(p, mem, n);
return p;
}
gchar *
g_strdup(const gchar *s)
{
return s ? (gchar *) g_memdup2(s, strlen(s) + 1) : NULL;
}
/* --------------------------------------------------------------- Messages */
/* Tout part sur la sortie d'erreur : la sortie standard appartient à
* l'application (JSON d'engine-cli). */
void
g_print(const gchar *format, ...)
{
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
}
void
g_printerr(const gchar *format, ...)
{
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
}
void
g_warning(const gchar *format, ...)
{
va_list args;
fputs("moteur (avertissement) : ", stderr);
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
fputc('\n', stderr);
}
void
g_error(const gchar *format, ...)
{
va_list args;
fputs("moteur (erreur fatale) : ", stderr);
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
fputc('\n', stderr);
abort();
}
/* ---------------------------------------------------------------- Chaînes */
/*
* strtod indépendant de la locale : le point est toujours le séparateur
* décimal. Si la locale courante utilise un autre caractère (la virgule en
* français), on le substitue au point dans une copie avant d'appeler strtod.
*/
gdouble
g_ascii_strtod(const gchar *nptr, gchar **endptr)
{
const char *point = localeconv()->decimal_point;
char *copy, *end;
double value;
size_t i;
if (!point || strlen(point) != 1 || point[0] == '.')
return strtod(nptr, endptr);
copy = g_strdup(nptr);
for (i = 0; copy[i]; i++)
if (copy[i] == '.')
copy[i] = point[0];
value = strtod(copy, &end);
if (endptr)
*endptr = (gchar *) nptr + (end - copy);
g_free(copy);
return value;
}
guint64
g_ascii_strtoull(const gchar *nptr, gchar **endptr, guint base)
{
return (guint64) strtoull(nptr, endptr, (int) base);
}
static int
ascii_tolower(int c)
{
return (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c;
}
gint
g_ascii_strncasecmp(const gchar *s1, const gchar *s2, gsize n)
{
for (; n > 0; n--, s1++, s2++) {
int c1 = ascii_tolower((unsigned char) *s1);
int c2 = ascii_tolower((unsigned char) *s2);
if (c1 != c2 || !c1)
return c1 - c2;
}
return 0;
}
gint
g_ascii_strcasecmp(const gchar *s1, const gchar *s2)
{
return g_ascii_strncasecmp(s1, s2, (gsize) -1);
}
const gchar *
g_strerror(gint errnum)
{
return strerror(errnum);
}
/* Copie tronquée et toujours terminée ; renvoie la longueur de src. */
gsize
g_strlcpy(gchar *dest, const gchar *src, gsize dest_size)
{
gsize len = strlen(src);
if (dest_size > 0) {
gsize n = len < dest_size - 1 ? len : dest_size - 1;
memcpy(dest, src, n);
dest[n] = '\0';
}
return len;
}
/* Assemble des morceaux de chemin avec « / », sans doubler le séparateur. */
gchar *
g_build_filename(const gchar *first, ...)
{
va_list args;
const gchar *part;
size_t total = 1;
gchar *result;
va_start(args, first);
for (part = first; part; part = va_arg(args, const gchar *))
total += strlen(part) + 1;
va_end(args);
result = g_malloc(total);
result[0] = '\0';
va_start(args, first);
for (part = first; part; part = va_arg(args, const gchar *)) {
size_t len = strlen(result);
if (!*part)
continue;
if (len > 0 && result[len - 1] != '/')
strcat(result, "/");
if (len > 0 && *part == '/')
part++;
strcat(result, part);
}
va_end(args);
return result;
}
/* --------------------------------------------------------------- Fichiers */
gboolean
g_file_test(const gchar *filename, GFileTest test)
{
struct stat st;
if (stat(filename, &st) != 0)
return FALSE;
if (test & G_FILE_TEST_EXISTS)
return TRUE;
if ((test & G_FILE_TEST_IS_REGULAR) && S_ISREG(st.st_mode))
return TRUE;
if ((test & G_FILE_TEST_IS_DIR) && S_ISDIR(st.st_mode))
return TRUE;
if ((test & G_FILE_TEST_IS_EXECUTABLE) && access(filename, X_OK) == 0)
return TRUE;
return FALSE;
}
/* Lit un fichier entier en mémoire, terminé par un octet nul. */
gboolean
g_file_get_contents(const gchar *filename, gchar **contents, gsize *length, GError **error)
{
FILE *f;
long size;
gchar *buffer;
*contents = NULL;
f = fopen(filename, "rb");
if (!f) {
set_error(error, errno, "lecture impossible", filename);
return FALSE;
}
if (fseek(f, 0, SEEK_END) != 0 || (size = ftell(f)) < 0 || fseek(f, 0, SEEK_SET) != 0) {
fclose(f);
set_error(error, EIO, "taille illisible", filename);
return FALSE;
}
buffer = g_malloc((size_t) size + 1);
if (fread(buffer, 1, (size_t) size, f) != (size_t) size) {
fclose(f);
g_free(buffer);
set_error(error, EIO, "lecture incomplète", filename);
return FALSE;
}
fclose(f);
buffer[size] = '\0';
*contents = buffer;
if (length)
*length = (gsize) size;
return TRUE;
}
/* Fichier projeté en mémoire en lecture seule (mmap). */
struct UstagammonMappedFile {
gchar *contents;
gsize length;
};
GMappedFile *
g_mapped_file_new(const gchar *filename, gboolean writable, GError **error)
{
GMappedFile *m;
struct stat st;
void *p = NULL;
int fd;
if (writable) {
set_error(error, EINVAL, "projection en écriture non prise en charge", filename);
return NULL;
}
fd = open(filename, O_RDONLY);
if (fd < 0) {
set_error(error, errno, "ouverture impossible", filename);
return NULL;
}
if (fstat(fd, &st) != 0) {
set_error(error, errno, "taille illisible", filename);
close(fd);
return NULL;
}
if (st.st_size > 0) {
p = mmap(NULL, (size_t) st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (p == MAP_FAILED) {
set_error(error, errno, "projection impossible", filename);
close(fd);
return NULL;
}
}
close(fd);
m = g_new0(GMappedFile, 1);
m->contents = p;
m->length = (gsize) st.st_size;
return m;
}
gchar *
g_mapped_file_get_contents(GMappedFile *file)
{
return file->contents;
}
gsize
g_mapped_file_get_length(GMappedFile *file)
{
return file->length;
}
void
g_mapped_file_unref(GMappedFile *file)
{
if (!file)
return;
if (file->contents)
munmap(file->contents, file->length);
g_free(file);
}
/* ------------------------------------------------------------- XML simple */
/*
* Analyseur XML réduit à ce qu'utilisent les tables d'équité de gnubg
* (fichiers XML de engine/data/) : éléments, attributs entre guillemets, texte,
* commentaires, prologue <?xml?> et <!DOCTYPE> sans sous-ensemble interne,
* entités &lt; &gt; &amp; &quot; &apos;. Tout le reste est une erreur.
*/
struct UstagammonMarkupParseContext {
const GMarkupParser *parser;
gpointer user_data;
GDestroyNotify user_data_dnotify;
};
GMarkupParseContext *
g_markup_parse_context_new(const GMarkupParser *parser, GMarkupParseFlags flags,
gpointer user_data, GDestroyNotify user_data_dnotify)
{
GMarkupParseContext *c = g_new0(GMarkupParseContext, 1);
(void) flags;
c->parser = parser;
c->user_data = user_data;
c->user_data_dnotify = user_data_dnotify;
return c;
}
void
g_markup_parse_context_free(GMarkupParseContext *context)
{
if (!context)
return;
if (context->user_data_dnotify)
context->user_data_dnotify(context->user_data);
g_free(context);
}
#define MAX_DEPTH 32
#define MAX_ATTRIBUTES 8
/* Copie [start, end) en décodant les entités ; NULL si une entité est inconnue. */
static gchar *
decode_text(const char *start, const char *end)
{
static const struct {
const char *name;
char c;
} entities[] = {
{ "&lt;", '<' }, { "&gt;", '>' }, { "&amp;", '&' }, { "&quot;", '"' }, { "&apos;", '\'' }
};
gchar *out = g_malloc((size_t) (end - start) + 1);
gchar *o = out;
const char *p = start;
while (p < end) {
size_t i, n = sizeof entities / sizeof entities[0];
if (*p != '&') {
*o++ = *p++;
continue;
}
for (i = 0; i < n; i++) {
size_t len = strlen(entities[i].name);
if ((size_t) (end - p) >= len && strncmp(p, entities[i].name, len) == 0) {
*o++ = entities[i].c;
p += len;
break;
}
}
if (i == n) {
g_free(out);
return NULL;
}
}
*o = '\0';
return out;
}
static int
is_name_char(char c)
{
return isalnum((unsigned char) c) || c == '-' || c == '_' || c == ':' || c == '.';
}
static const char *
skip_spaces(const char *p, const char *end)
{
while (p < end && isspace((unsigned char) *p))
p++;
return p;
}
/* Copie le nom qui commence en p ; avance *after juste derrière. */
static gchar *
read_name(const char *p, const char *end, const char **after)
{
const char *q = p;
gchar *name;
while (q < end && is_name_char(*q))
q++;
*after = q;
if (q == p)
return NULL;
name = g_malloc((size_t) (q - p) + 1);
memcpy(name, p, (size_t) (q - p));
name[q - p] = '\0';
return name;
}
/*
* Lit les attributs d'une balise ouvrante jusqu'à « > » ou « /> ».
* Renvoie la position après la balise, ou NULL en cas d'erreur.
*/
static const char *
read_attributes(const char *p, const char *end, gchar **names, gchar **values, int *empty)
{
int n = 0;
names[0] = values[0] = NULL;
for (;;) {
const char *value_start;
char quote;
p = skip_spaces(p, end);
if (p >= end)
return NULL;
if (*p == '>')
return p + 1;
if (*p == '/' && p + 1 < end && p[1] == '>') {
*empty = 1;
return p + 2;
}
if (n == MAX_ATTRIBUTES)
return NULL;
names[n] = read_name(p, end, &p);
names[n + 1] = values[n] = values[n + 1] = NULL;
if (!names[n])
return NULL;
p = skip_spaces(p, end);
if (p >= end || *p != '=')
return NULL;
p = skip_spaces(p + 1, end);
if (p >= end || (*p != '"' && *p != '\''))
return NULL;
quote = *p++;
value_start = p;
while (p < end && *p != quote)
p++;
if (p >= end)
return NULL;
values[n] = decode_text(value_start, p);
if (!values[n])
return NULL;
n++;
p++;
}
}
static void
free_strings(gchar **strings)
{
for (; *strings; strings++)
g_free(*strings);
}
gboolean
g_markup_parse_context_parse(GMarkupParseContext *context, const gchar *text,
gssize text_len, GError **error)
{
const GMarkupParser *mp = context->parser;
const char *p = text;
const char *end = text + (text_len < 0 ? (gssize) strlen(text) : text_len);
gchar *stack[MAX_DEPTH];
int depth = 0;
const char *problem = NULL;
GError *err = NULL;
while (p < end && !problem && !err) {
if (*p != '<') {
/* Texte jusqu'à la prochaine balise. */
const char *start = p;
gchar *t;
while (p < end && *p != '<')
p++;
if (depth == 0) {
if (skip_spaces(start, p) != p)
problem = "texte hors de l'élément racine";
continue;
}
t = decode_text(start, p);
if (!t)
problem = "entité inconnue";
else if (mp->text)
mp->text(context, t, strlen(t), context->user_data, &err);
g_free(t);
} else if (end - p >= 4 && strncmp(p, "<!--", 4) == 0) {
const char *q = p + 4;
while (q + 3 <= end && strncmp(q, "-->", 3) != 0)
q++;
if (q + 3 > end)
problem = "commentaire non terminé";
p = q + 3;
} else if (end - p >= 2 && (p[1] == '?' || p[1] == '!')) {
const char *q = memchr(p, '>', (size_t) (end - p));
if (!q || memchr(p, '[', (size_t) (q - p)))
problem = "déclaration non prise en charge";
else
p = q + 1;
} else if (end - p >= 2 && p[1] == '/') {
/* Balise fermante. */
gchar *name = read_name(p + 2, end, &p);
p = skip_spaces(p, end);
if (!name || p >= end || *p != '>')
problem = "balise fermante mal formée";
else if (depth == 0 || strcmp(stack[depth - 1], name) != 0)
problem = "balise fermante inattendue";
else {
depth--;
if (mp->end_element)
mp->end_element(context, name, context->user_data, &err);
g_free(stack[depth]);
p++;
}
g_free(name);
} else {
/* Balise ouvrante, éventuellement vide (<a/>). */
gchar *names[MAX_ATTRIBUTES + 1], *values[MAX_ATTRIBUTES + 1];
int empty = 0;
gchar *name = read_name(p + 1, end, &p);
names[0] = values[0] = NULL;
if (name)
p = read_attributes(p, end, names, values, &empty);
if (!name || !p)
problem = "balise ouvrante mal formée";
else if (depth == MAX_DEPTH)
problem = "imbrication trop profonde";
else {
if (mp->start_element)
mp->start_element(context, name, (const gchar **) names,
(const gchar **) values, context->user_data, &err);
if (!err && empty && mp->end_element)
mp->end_element(context, name, context->user_data, &err);
}
free_strings(names);
free_strings(values);
if (!problem && !err && !empty)
stack[depth++] = name;
else
g_free(name);
}
}
if (!problem && !err && depth > 0)
problem = "document incomplet";
while (depth > 0)
g_free(stack[--depth]);
if (problem)
err = new_error(0, "XML invalide", problem);
if (err) {
if (mp->error)
mp->error(context, err, context->user_data);
if (error)
*error = err;
else
g_error_free(err);
return FALSE;
}
return TRUE;
}