/* * 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 #include #include #include #include #include #include #include #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 et sans sous-ensemble interne, * entités < > & " '. 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[] = { { "<", '<' }, { ">", '>' }, { "&", '&' }, { """, '"' }, { "'", '\'' } }; 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, "", 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 (). */ 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; }