Coverage Report

Created: 2025-08-24 07:10

/src/libxmlb/src/xb-query.c
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright 2018 Richard Hughes <richard@hughsie.com>
3
 *
4
 * SPDX-License-Identifier: LGPL-2.1-or-later
5
 */
6
7
0
#define G_LOG_DOMAIN "XbSilo"
8
9
#include "config.h"
10
11
#include <gio/gio.h>
12
13
#include "xb-machine.h"
14
#include "xb-opcode-private.h"
15
#include "xb-query-private.h"
16
#include "xb-silo-private.h"
17
#include "xb-stack-private.h"
18
19
typedef struct {
20
  GPtrArray *sections; /* of XbQuerySection */
21
  XbQueryFlags flags;
22
  gchar *xpath;
23
  guint limit;
24
} XbQueryPrivate;
25
26
G_DEFINE_TYPE_WITH_PRIVATE(XbQuery, xb_query, G_TYPE_OBJECT)
27
0
#define GET_PRIVATE(o) (xb_query_get_instance_private(o))
28
29
typedef struct {
30
  XbSilo *silo;
31
} XbQueryParseContext;
32
33
/**
34
 * xb_query_get_sections:
35
 * @self: a #XbQuery
36
 *
37
 * Gets the sections that make up the query.
38
 *
39
 * Returns: (transfer none) (element-type XbQuerySection): sections
40
 *
41
 * Since: 0.1.4
42
 **/
43
GPtrArray *
44
xb_query_get_sections(XbQuery *self)
45
0
{
46
0
  XbQueryPrivate *priv = GET_PRIVATE(self);
47
0
  g_return_val_if_fail(XB_IS_QUERY(self), NULL);
48
0
  return priv->sections;
49
0
}
50
51
/**
52
 * xb_query_get_xpath:
53
 * @self: a #XbQuery
54
 *
55
 * Gets the XPath string that created the query.
56
 *
57
 * Returns: string
58
 *
59
 * Since: 0.1.4
60
 **/
61
const gchar *
62
xb_query_get_xpath(XbQuery *self)
63
0
{
64
0
  XbQueryPrivate *priv = GET_PRIVATE(self);
65
0
  g_return_val_if_fail(XB_IS_QUERY(self), NULL);
66
0
  return priv->xpath;
67
0
}
68
69
static gchar *
70
xb_query_section_to_string(XbQuerySection *sect)
71
0
{
72
0
  GString *str = g_string_new(NULL);
73
0
  if (sect->kind == XB_SILO_QUERY_KIND_PARENT)
74
0
    g_string_append(str, "..");
75
0
  else if (sect->kind == XB_SILO_QUERY_KIND_WILDCARD)
76
0
    g_string_append(str, "*");
77
0
  else
78
0
    g_string_append(str, sect->element);
79
0
  if (sect->predicates != NULL && sect->predicates->len > 0) {
80
0
    g_string_append(str, "[");
81
0
    for (guint j = 0; j < sect->predicates->len; j++) {
82
0
      XbStack *stack = g_ptr_array_index(sect->predicates, j);
83
0
      g_autofree gchar *tmp = xb_stack_to_string(stack);
84
0
      g_string_append(str, tmp);
85
0
    }
86
0
    g_string_append(str, "]");
87
0
  }
88
0
  return g_string_free(str, FALSE);
89
0
}
90
91
/**
92
 * xb_query_to_string:
93
 * @self: a #XbQuery
94
 *
95
 * Gets the XPath that was used for the query.
96
 *
97
 * Returns: string
98
 *
99
 * Since: 0.1.13
100
 **/
101
gchar *
102
xb_query_to_string(XbQuery *self)
103
0
{
104
0
  XbQueryPrivate *priv = GET_PRIVATE(self);
105
0
  GString *str = g_string_new(NULL);
106
0
  for (guint i = 0; i < priv->sections->len; i++) {
107
0
    XbQuerySection *sect = g_ptr_array_index(priv->sections, i);
108
0
    g_autofree gchar *tmp = xb_query_section_to_string(sect);
109
0
    g_string_append(str, tmp);
110
0
    if (i != priv->sections->len - 1)
111
0
      g_string_append(str, "/");
112
0
  }
113
0
  return g_string_free(str, FALSE);
114
0
}
115
116
/**
117
 * xb_query_get_limit:
118
 * @self: a #XbQuery
119
 *
120
 * Gets the results limit on this query, where 0 is 'all'.
121
 *
122
 * Returns: integer, default 0
123
 *
124
 * Deprecated: 0.3.0: This is not thread-safe. Use xb_query_context_get_limit()
125
 *     instead.
126
 * Since: 0.1.4
127
 **/
128
guint
129
xb_query_get_limit(XbQuery *self)
130
0
{
131
0
  XbQueryPrivate *priv = GET_PRIVATE(self);
132
0
  g_return_val_if_fail(XB_IS_QUERY(self), 0);
133
0
  return priv->limit;
134
0
}
135
136
/**
137
 * xb_query_set_limit:
138
 * @self: a #XbQuery
139
 * @limit: integer
140
 *
141
 * Sets the results limit on this query, where 0 is 'all'.
142
 *
143
 * Deprecated: 0.3.0: This is not thread-safe. Use xb_query_context_set_limit()
144
 *     instead.
145
 * Since: 0.1.4
146
 **/
147
void
148
xb_query_set_limit(XbQuery *self, guint limit)
149
0
{
150
0
  XbQueryPrivate *priv = GET_PRIVATE(self);
151
0
  g_return_if_fail(XB_IS_QUERY(self));
152
0
  priv->limit = limit;
153
0
}
154
155
/**
156
 * xb_query_get_flags:
157
 * @self: a #XbQuery
158
 *
159
 * Gets the flags used for this query.
160
 *
161
 * Returns: #XbQueryFlags, default %XB_QUERY_FLAG_NONE
162
 *
163
 * Deprecated: 0.3.0: This is not thread-safe. Use xb_query_context_get_flags()
164
 *     instead.
165
 * Since: 0.1.15
166
 **/
167
XbQueryFlags
168
xb_query_get_flags(XbQuery *self)
169
0
{
170
0
  XbQueryPrivate *priv = GET_PRIVATE(self);
171
0
  g_return_val_if_fail(XB_IS_QUERY(self), 0);
172
0
  return priv->flags;
173
0
}
174
175
/**
176
 * xb_query_set_flags:
177
 * @self: a #XbQuery
178
 * @flags: a #XbQueryFlags, e.g. %XB_QUERY_FLAG_USE_INDEXES
179
 *
180
 * Sets the flags to use for this query.
181
 *
182
 * Deprecated: 0.3.0: This is not thread-safe. Use xb_query_context_set_flags()
183
 *     instead.
184
 * Since: 0.1.15
185
 **/
186
void
187
xb_query_set_flags(XbQuery *self, XbQueryFlags flags)
188
0
{
189
0
  XbQueryPrivate *priv = GET_PRIVATE(self);
190
0
  g_return_if_fail(XB_IS_QUERY(self));
191
0
  priv->flags = flags;
192
0
}
193
194
static XbOpcode *
195
xb_query_get_bound_opcode(XbQuery *self, guint idx)
196
0
{
197
0
  XbQueryPrivate *priv = GET_PRIVATE(self);
198
0
  guint idx_cnt = 0;
199
200
0
  for (guint i = 0; i < priv->sections->len; i++) {
201
0
    XbQuerySection *section = g_ptr_array_index(priv->sections, i);
202
0
    if (section->predicates == NULL)
203
0
      continue;
204
0
    for (guint j = 0; j < section->predicates->len; j++) {
205
0
      XbStack *stack = g_ptr_array_index(section->predicates, j);
206
0
      for (guint k = 0; k < xb_stack_get_size(stack); k++) {
207
0
        XbOpcode *op = xb_stack_peek(stack, k);
208
0
        if (xb_opcode_is_binding(op)) {
209
0
          if (idx == idx_cnt++)
210
0
            return op;
211
0
        }
212
0
      }
213
0
    }
214
0
  }
215
0
  return NULL;
216
0
}
217
218
/**
219
 * xb_query_bind_str:
220
 * @self: a #XbQuery
221
 * @idx: an integer index
222
 * @str: string to assign to the bound variable
223
 * @error: a #GError, or %NULL
224
 *
225
 * Assigns a string to a bound value specified using `?`.
226
 *
227
 * Returns: %TRUE if the @idx existed
228
 *
229
 * Since: 0.1.4
230
 * Deprecated: 0.3.0: Use #XbValueBindings and xb_value_bindings_bind_str()
231
 *     instead. That keeps the value bindings separate from the #XbQuery,
232
 *     allowing queries to be re-used over time and between threads.
233
 **/
234
gboolean
235
xb_query_bind_str(XbQuery *self, guint idx, const gchar *str, GError **error)
236
0
{
237
0
  G_GNUC_BEGIN_IGNORE_DEPRECATIONS
238
0
  XbOpcode *op;
239
240
0
  g_return_val_if_fail(XB_IS_QUERY(self), FALSE);
241
0
  g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
242
243
  /* get the correct opcode */
244
0
  op = xb_query_get_bound_opcode(self, idx);
245
0
  if (op == NULL) {
246
0
    g_set_error(error,
247
0
          G_IO_ERROR,
248
0
          G_IO_ERROR_INVALID_ARGUMENT,
249
0
          "no bound opcode with index %u",
250
0
          idx);
251
0
    return FALSE;
252
0
  }
253
0
  xb_opcode_bind_str(op, g_strdup(str), g_free);
254
0
  return TRUE;
255
0
  G_GNUC_END_IGNORE_DEPRECATIONS
256
0
}
257
258
/**
259
 * xb_query_bind_val:
260
 * @self: a #XbQuery
261
 * @idx: an integer index
262
 * @val: value to assign to the bound variable
263
 * @error: a #GError, or %NULL
264
 *
265
 * Assigns a string to a bound value specified using `?`.
266
 *
267
 * Returns: %TRUE if the @idx existed
268
 *
269
 * Since: 0.1.4
270
 * Deprecated: 0.3.0: Use #XbValueBindings and xb_value_bindings_bind_val()
271
 *     instead. That keeps the value bindings separate from the #XbQuery,
272
 *     allowing queries to be re-used over time and between threads.
273
 **/
274
gboolean
275
xb_query_bind_val(XbQuery *self, guint idx, guint32 val, GError **error)
276
0
{
277
0
  G_GNUC_BEGIN_IGNORE_DEPRECATIONS
278
0
  XbOpcode *op;
279
280
0
  g_return_val_if_fail(XB_IS_QUERY(self), FALSE);
281
0
  g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
282
283
  /* get the correct opcode */
284
0
  op = xb_query_get_bound_opcode(self, idx);
285
0
  if (op == NULL) {
286
0
    g_set_error(error,
287
0
          G_IO_ERROR,
288
0
          G_IO_ERROR_INVALID_ARGUMENT,
289
0
          "no bound opcode with index %u",
290
0
          idx);
291
0
    return FALSE;
292
0
  }
293
0
  xb_opcode_bind_val(op, val);
294
0
  return TRUE;
295
0
  G_GNUC_END_IGNORE_DEPRECATIONS
296
0
}
297
298
static void
299
xb_query_section_free(XbQuerySection *section)
300
0
{
301
0
  if (section->predicates != NULL)
302
0
    g_ptr_array_unref(section->predicates);
303
0
  g_free(section->element);
304
0
  g_slice_free(XbQuerySection, section);
305
0
}
306
307
G_DEFINE_AUTOPTR_CLEANUP_FUNC(XbQuerySection, xb_query_section_free)
308
309
static gboolean
310
xb_query_repair_opcode_texi(XbQuery *self,
311
          XbQueryParseContext *context,
312
          XbOpcode *op,
313
          GError **error)
314
0
{
315
0
  if (xb_opcode_get_val(op) == XB_SILO_UNSET) {
316
0
    const gchar *tmp = xb_opcode_get_str(op);
317
0
    guint32 val = xb_silo_strtab_index_lookup(context->silo, tmp);
318
0
    if (val == XB_SILO_UNSET) {
319
0
      g_set_error(error,
320
0
            G_IO_ERROR,
321
0
            G_IO_ERROR_INVALID_ARGUMENT,
322
0
            "indexed string '%s' was unfound",
323
0
            tmp);
324
0
      return FALSE;
325
0
    }
326
0
    xb_opcode_set_val(op, val);
327
0
  }
328
0
  return TRUE;
329
0
}
330
331
/* Returns an error if the XPath is invalid. */
332
static gboolean
333
xb_query_parse_predicate(XbQuery *self,
334
       XbQueryParseContext *context,
335
       XbQuerySection *section,
336
       const gchar *text,
337
       gssize text_len,
338
       GError **error)
339
0
{
340
0
  XbQueryPrivate *priv = GET_PRIVATE(self);
341
0
  XbMachineParseFlags machine_flags = XB_MACHINE_PARSE_FLAG_NONE;
342
0
  g_autoptr(XbStack) opcodes = NULL;
343
344
  /* set flags */
345
0
  if (priv->flags & XB_QUERY_FLAG_OPTIMIZE)
346
0
    machine_flags |= XB_MACHINE_PARSE_FLAG_OPTIMIZE;
347
348
  /* parse */
349
0
  opcodes = xb_machine_parse_full(xb_silo_get_machine(context->silo),
350
0
          text,
351
0
          text_len,
352
0
          machine_flags,
353
0
          error);
354
0
  if (opcodes == NULL)
355
0
    return FALSE;
356
357
  /* repair or convert the indexed strings */
358
0
  if (priv->flags & XB_QUERY_FLAG_USE_INDEXES) {
359
0
    for (guint i = 0; i < xb_stack_get_size(opcodes); i++) {
360
0
      XbOpcode *op = xb_stack_peek(opcodes, i);
361
0
      if (xb_opcode_get_kind(op) != XB_OPCODE_KIND_INDEXED_TEXT)
362
0
        continue;
363
0
      if (!xb_query_repair_opcode_texi(self, context, op, error))
364
0
        return FALSE;
365
0
    }
366
0
  } else {
367
0
    for (guint i = 0; i < xb_stack_get_size(opcodes); i++) {
368
0
      XbOpcode *op = xb_stack_peek(opcodes, i);
369
0
      if (xb_opcode_get_kind(op) == XB_OPCODE_KIND_INDEXED_TEXT)
370
0
        xb_opcode_set_kind(op, XB_OPCODE_KIND_TEXT);
371
0
    }
372
0
  }
373
374
  /* create array if it does not exist */
375
0
  if (section->predicates == NULL)
376
0
    section->predicates =
377
0
        g_ptr_array_new_with_free_func((GDestroyNotify)xb_stack_unref);
378
0
  g_ptr_array_add(section->predicates, g_steal_pointer(&opcodes));
379
0
  return TRUE;
380
0
}
381
382
/* Returns an error if the XPath is invalid. */
383
static XbQuerySection *
384
xb_query_parse_section(XbQuery *self,
385
           XbQueryParseContext *context,
386
           const gchar *xpath,
387
           GError **error)
388
0
{
389
0
  g_autoptr(XbQuerySection) section = g_slice_new0(XbQuerySection);
390
0
  guint start = 0;
391
392
  /* common XPath sections */
393
0
  if (g_strcmp0(xpath, "parent::*") == 0 || g_strcmp0(xpath, "..") == 0) {
394
0
    section->kind = XB_SILO_QUERY_KIND_PARENT;
395
0
    return g_steal_pointer(&section);
396
0
  }
397
398
  /* parse element and predicate */
399
0
  for (guint i = 0; xpath[i] != '\0'; i++) {
400
0
    if (start == 0 && xpath[i] == '[') {
401
0
      if (section->element == NULL)
402
0
        section->element = g_strndup(xpath, i);
403
0
      start = i;
404
0
      continue;
405
0
    }
406
0
    if (start > 0 && xpath[i] == ']') {
407
0
      if (!xb_query_parse_predicate(self,
408
0
                  context,
409
0
                  section,
410
0
                  xpath + start + 1,
411
0
                  i - start - 1,
412
0
                  error)) {
413
0
        return NULL;
414
0
      }
415
0
      start = 0;
416
0
      continue;
417
0
    }
418
0
  }
419
420
  /* incomplete predicate */
421
0
  if (start != 0) {
422
0
    g_set_error(error,
423
0
          G_IO_ERROR,
424
0
          G_IO_ERROR_INVALID_ARGUMENT,
425
0
          "predicate %s was unfinished, missing ']'",
426
0
          xpath + start);
427
0
    return NULL;
428
0
  }
429
430
0
  if (section->element == NULL)
431
0
    section->element = g_strdup(xpath);
432
0
  if (g_strcmp0(section->element, "child::*") == 0 || g_strcmp0(section->element, "*") == 0) {
433
0
    section->kind = XB_SILO_QUERY_KIND_WILDCARD;
434
0
    return g_steal_pointer(&section);
435
0
  }
436
437
  /* This may result in @element_idx being set to %XB_SILO_UNSET if the
438
   * given element (`section->element`) is not in the silo at all. Ignore
439
   * that for now, and return no matches when the query is actually run. */
440
0
  section->element_idx = xb_silo_get_strtab_idx(context->silo, section->element);
441
442
0
  return g_steal_pointer(&section);
443
0
}
444
445
/* Returns an error if the XPath is invalid. */
446
static gboolean
447
xb_query_parse(XbQuery *self, XbQueryParseContext *context, const gchar *xpath, GError **error)
448
0
{
449
0
  XbQueryPrivate *priv = GET_PRIVATE(self);
450
0
  XbQuerySection *section;
451
0
  g_autoptr(GString) acc = g_string_new(NULL);
452
453
  //  g_debug ("parsing XPath %s", xpath);
454
0
  for (gsize i = 0; xpath[i] != '\0'; i++) {
455
    /* escaped chars */
456
0
    if (xpath[i] == '\\') {
457
0
      if (xpath[i + 1] == '/' || xpath[i + 1] == 't' || xpath[i + 1] == 'n') {
458
0
        g_string_append_c(acc, xpath[i + 1]);
459
0
        i += 1;
460
0
        continue;
461
0
      }
462
0
    }
463
464
    /* split */
465
0
    if (xpath[i] == '/') {
466
0
      if (acc->len == 0) {
467
0
        g_set_error_literal(error,
468
0
                G_IO_ERROR,
469
0
                G_IO_ERROR_NOT_FOUND,
470
0
                "xpath section empty");
471
0
        return FALSE;
472
0
      }
473
0
      section = xb_query_parse_section(self, context, acc->str, error);
474
0
      if (section == NULL)
475
0
        return FALSE;
476
0
      g_ptr_array_add(priv->sections, section);
477
0
      g_string_truncate(acc, 0);
478
0
      continue;
479
0
    }
480
0
    g_string_append_c(acc, xpath[i]);
481
0
  }
482
483
  /* add any remaining section */
484
0
  section = xb_query_parse_section(self, context, acc->str, error);
485
0
  if (section == NULL)
486
0
    return FALSE;
487
0
  g_ptr_array_add(priv->sections, section);
488
0
  return TRUE;
489
0
}
490
491
/**
492
 * xb_query_new_full:
493
 * @silo: a #XbSilo
494
 * @xpath: The XPath query
495
 * @flags: some #XbQueryFlags, e.g. #XB_QUERY_FLAG_USE_INDEXES
496
 * @error: the #GError, or %NULL
497
 *
498
 * Creates a query to be used by @silo. It may be quicker to create a query
499
 * manually and re-use it multiple times.
500
 *
501
 * The query will point to strings inside @silo, so the lifetime of @silo must
502
 * exceed the lifetime of the returned query.
503
 *
504
 * Returns: (transfer full): a #XbQuery
505
 *
506
 * Since: 0.1.6
507
 **/
508
XbQuery *
509
xb_query_new_full(XbSilo *silo, const gchar *xpath, XbQueryFlags flags, GError **error)
510
0
{
511
0
  g_autoptr(XbQuery) self = g_object_new(XB_TYPE_QUERY, NULL);
512
0
  XbQueryPrivate *priv = GET_PRIVATE(self);
513
0
  XbQueryParseContext parse_context = {
514
0
      .silo = silo,
515
0
  };
516
517
0
  g_return_val_if_fail(XB_IS_SILO(silo), NULL);
518
0
  g_return_val_if_fail(error == NULL || *error == NULL, NULL);
519
520
  /* create; don’t take a reference on @silo otherwise we get refcount
521
   * loops with cached queries from xb_silo_lookup_query() */
522
0
  priv->xpath = g_strdup(xpath);
523
0
  priv->flags = flags;
524
0
  priv->sections = g_ptr_array_new_with_free_func((GDestroyNotify)xb_query_section_free);
525
526
  /* add each section */
527
0
  if (!xb_query_parse(self, &parse_context, xpath, error))
528
0
    return NULL;
529
530
  /* nothing here! */
531
0
  if (priv->sections->len == 0) {
532
0
    g_set_error(error,
533
0
          G_IO_ERROR,
534
0
          G_IO_ERROR_NOT_SUPPORTED,
535
0
          "No query sections for '%s'",
536
0
          xpath);
537
0
    return NULL;
538
0
  }
539
540
  /* success */
541
0
  return g_steal_pointer(&self);
542
0
}
543
544
/**
545
 * xb_query_new:
546
 * @silo: a #XbSilo
547
 * @xpath: The XPath query
548
 * @error: the #GError, or %NULL
549
 *
550
 * Creates a query to be used by @silo. It may be quicker to create a query
551
 * manually and re-use it multiple times.
552
 *
553
 * Returns: (transfer full): a #XbQuery
554
 *
555
 * Since: 0.1.4
556
 **/
557
XbQuery *
558
xb_query_new(XbSilo *silo, const gchar *xpath, GError **error)
559
0
{
560
0
  return xb_query_new_full(silo,
561
0
         xpath,
562
0
         XB_QUERY_FLAG_OPTIMIZE | XB_QUERY_FLAG_USE_INDEXES,
563
0
         error);
564
0
}
565
566
static void
567
xb_query_init(XbQuery *self)
568
0
{
569
0
}
570
571
static void
572
xb_query_finalize(GObject *obj)
573
0
{
574
0
  XbQuery *self = XB_QUERY(obj);
575
0
  XbQueryPrivate *priv = GET_PRIVATE(self);
576
0
  g_ptr_array_unref(priv->sections);
577
0
  g_free(priv->xpath);
578
0
  G_OBJECT_CLASS(xb_query_parent_class)->finalize(obj);
579
0
}
580
581
static void
582
xb_query_class_init(XbQueryClass *klass)
583
0
{
584
0
  GObjectClass *object_class = G_OBJECT_CLASS(klass);
585
0
  object_class->finalize = xb_query_finalize;
586
0
}