Coverage Report

Created: 2026-08-14 06:45

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/wireshark/epan/dissectors/json-dictionary.c
Line
Count
Source
1
/* json-dictionary.c
2
 * JSON dictionary parsing for JSON protocol dissector
3
 * Copyright 2026, Mark Stout <mark.stout@markstout.com>
4
 *
5
 * Wireshark - Network traffic analyzer
6
 * By Gerald Combs <gerald@wireshark.org>
7
 * Copyright 1998 Gerald Combs
8
 *
9
 * SPDX-License-Identifier: GPL-2.0-or-later
10
 */
11
12
/*
13
 * JSON Dictionary XML Parser
14
 *
15
 * Loads dictionary files that define JSON field mappings, similar to
16
 * how the Diameter dissector loads AVP definitions.
17
 */
18
19
#include "config.h"
20
21
#include <string.h>
22
#include <stdio.h>
23
24
#include <glib.h>
25
#include <libxml/parser.h>
26
#include <libxml/tree.h>
27
28
#include <epan/packet.h>
29
#include <epan/prefs.h>
30
#include <epan/expert.h>
31
#include <wsutil/regex.h>
32
#include <wsutil/wslog.h>
33
#include <wsutil/filesystem.h>
34
#include <wsutil/file_util.h>
35
#include <wsutil/report_message.h>
36
37
#include "json-dictionary.h"
38
39
/* Base type mapping */
40
typedef struct _base_type_mapping {
41
  const char *name;
42
  json_field_type_t type;
43
  enum ftenum ft_type;
44
  int display_base;
45
} base_type_mapping_t;
46
47
static const base_type_mapping_t base_type_mappings[] = {
48
  { "string",    JSON_FIELD_STRING,   FT_STRING,  BASE_NONE },
49
  { "int64",     JSON_FIELD_INTEGER,  FT_INT64,   BASE_DEC },
50
  { "uint64",    JSON_FIELD_UNSIGNED, FT_UINT64,  BASE_DEC },
51
  { "int32",     JSON_FIELD_INTEGER,  FT_INT32,   BASE_DEC },
52
  { "uint32",    JSON_FIELD_UNSIGNED, FT_UINT32,  BASE_DEC },
53
  { "double",    JSON_FIELD_FLOAT,    FT_DOUBLE,  BASE_NONE },
54
  { "float",     JSON_FIELD_FLOAT,    FT_FLOAT,   BASE_NONE },
55
  { "boolean",   JSON_FIELD_BOOLEAN,  FT_BOOLEAN, BASE_NONE },
56
  { "object",    JSON_FIELD_OBJECT,   FT_NONE,    BASE_NONE },
57
  { "array",     JSON_FIELD_ARRAY,    FT_NONE,    BASE_NONE },
58
  { NULL,        JSON_FIELD_STRING,   FT_STRING,  BASE_NONE }
59
};
60
61
// Type definition storage passed as parameter
62
63
void
64
dict_type_def_free(dict_type_def_t *type_def)
65
0
{
66
0
  if (!type_def) return;
67
68
0
  xmlFree(type_def->type_name);
69
0
  xmlFree(type_def->base_type);
70
0
  xmlFree(type_def->display);
71
0
  g_free(type_def);
72
0
}
73
74
// Free enum definition
75
void
76
dict_enum_def_free(dict_enum_def_t *enum_def)
77
0
{
78
0
  if (!enum_def) return;
79
80
0
  xmlFree(enum_def->value);
81
0
  xmlFree(enum_def->description);
82
0
  g_free(enum_def);
83
0
}
84
85
/* Forward declaration — dict_wildcardfield_def_free and dict_field_def_free are mutually recursive */
86
static void dict_wildcardfield_def_free(dict_wildcardfield_def_t *wf);
87
88
// Free field definition recursively
89
void
90
dict_field_def_free(dict_field_def_t *field_def)
91
0
{
92
0
  if (!field_def) return;
93
94
0
  xmlFree(field_def->name);
95
0
  xmlFree(field_def->path);
96
0
  xmlFree(field_def->type);
97
0
  xmlFree(field_def->description);
98
0
  xmlFree(field_def->info_label);
99
0
  xmlFree(field_def->display_filter);
100
0
  xmlFree(field_def->parser);
101
0
  xmlFree(field_def->parser_args);
102
0
  xmlFree(field_def->case_attr);
103
104
0
  g_slist_free_full(field_def->child_fields, (GDestroyNotify)dict_field_def_free);
105
0
  g_slist_free_full(field_def->enum_values, (GDestroyNotify)dict_enum_def_free);
106
0
  g_slist_free_full(field_def->wildcard_children, (GDestroyNotify)dict_wildcardfield_def_free);
107
108
0
  g_free(field_def);
109
0
}
110
111
static void
112
dict_wildcardfield_def_free(dict_wildcardfield_def_t *wf)
113
0
{
114
0
  if (!wf) return;
115
0
  xmlFree(wf->name);
116
0
  xmlFree(wf->path);
117
0
  xmlFree(wf->alias);
118
0
  xmlFree(wf->display_value);
119
0
  xmlFree(wf->match);
120
0
  g_slist_free_full(wf->child_fields, (GDestroyNotify)dict_field_def_free);
121
0
  g_slist_free_full(wf->child_wildcards, (GDestroyNotify)dict_wildcardfield_def_free);
122
0
  g_free(wf);
123
0
}
124
125
// Free protocol definition
126
void
127
dict_protocol_def_free(dict_protocol_def_t *proto_def)
128
0
{
129
0
  if (!proto_def) return;
130
131
0
  xmlFree(proto_def->name);
132
0
  xmlFree(proto_def->display_name);
133
0
  xmlFree(proto_def->transport);
134
0
  xmlFree(proto_def->path);
135
0
  xmlFree(proto_def->case_attr);
136
0
  xmlFree(proto_def->condition_attr);
137
138
0
  g_slist_free_full(proto_def->content_types, (GDestroyNotify)xmlFree);
139
0
  g_slist_free_full(proto_def->fields, (GDestroyNotify)dict_field_def_free);
140
0
  g_slist_free_full(proto_def->ports, (GDestroyNotify)g_free);
141
142
0
  g_free(proto_def);
143
0
}
144
145
// Walk path_matchers and free the heap-allocated regex objects + pattern
146
// strings. The json_protocol_t structs themselves live in wmem_epan_scope and
147
// are released by wmem; only the ws_regex_t and the g_strdup'd pattern need
148
// explicit cleanup. Safe to call when path_matchers is NULL.
149
void
150
json_dictionary_cleanup(json_dictionary_t *dict)
151
0
{
152
0
  if (!dict) return;
153
154
  /* Free regex objects (only protocols with a path regex are in
155
   * path_matchers). */
156
0
  for (GSList *m = dict->path_matchers; m; m = m->next) {
157
0
    json_protocol_t *p = (json_protocol_t *)m->data;
158
0
    if (p->path_regex) {
159
0
      ws_regex_free(p->path_regex);
160
0
      p->path_regex = NULL;
161
0
    }
162
0
  }
163
0
  g_slist_free(dict->path_matchers);
164
0
  dict->path_matchers = NULL;
165
166
  /* Free the port_list GSList nodes on every protocol (the unsigned*
167
   * entries themselves are wmem-managed so we don't free them, just the
168
   * linked-list scaffolding). */
169
0
  for (GSList *e = dict->all_protocols; e; e = e->next) {
170
0
    json_protocol_t *p = (json_protocol_t *)e->data;
171
0
    if (p->port_list) {
172
0
      g_slist_free(p->port_list);
173
0
      p->port_list = NULL;
174
0
    }
175
0
  }
176
0
  g_slist_free(dict->all_protocols);
177
0
  dict->all_protocols = NULL;
178
179
  /* Free compiled regexes for <wildcardfield> match= attributes */
180
0
  for (GSList *wr = dict->wildcard_regexes; wr; wr = wr->next) {
181
0
    ws_regex_free((struct _ws_regex *)wr->data);
182
0
  }
183
0
  g_slist_free(dict->wildcard_regexes);
184
0
  dict->wildcard_regexes = NULL;
185
0
}
186
187
// Look up base type mapping
188
static const base_type_mapping_t *
189
lookup_base_type(const char *type_name)
190
0
{
191
0
  int i;
192
193
0
  if (!type_name) {
194
0
    return &base_type_mappings[0];  /* Default to string */
195
0
  }
196
197
0
  for (i = 0; base_type_mappings[i].name != NULL; i++) {
198
0
    if (strcmp(type_name, base_type_mappings[i].name) == 0) {
199
0
      return &base_type_mappings[i];
200
0
    }
201
0
  }
202
203
0
  return &base_type_mappings[0];  /* Default to string */
204
0
}
205
206
// Process typedefn element
207
static dict_type_def_t *
208
process_typedefn(xmlNodePtr node)
209
0
{
210
0
  dict_type_def_t *type_def;
211
0
  xmlChar *type_name, *base_type, *display;
212
213
0
  type_name = xmlGetProp(node, (const xmlChar *)XML_ATTR_TYPE_NAME);
214
0
  base_type = xmlGetProp(node, (const xmlChar *)XML_ATTR_BASE_TYPE);
215
0
  display = xmlGetProp(node, (const xmlChar *)XML_ATTR_DISPLAY);
216
217
0
  if (!type_name || !base_type) {
218
0
    xmlFree(type_name);
219
0
    xmlFree(base_type);
220
0
    xmlFree(display);
221
0
    return NULL;
222
0
  }
223
224
0
  type_def = g_new0(dict_type_def_t, 1);
225
0
  type_def->type_name = type_name;
226
0
  type_def->base_type = base_type;
227
0
  type_def->display = display;
228
229
0
  return type_def;
230
0
}
231
232
// Process enum element
233
static dict_enum_def_t *
234
process_enum(xmlNodePtr node)
235
0
{
236
0
  dict_enum_def_t *enum_def;
237
0
  xmlChar *name, *code_str, *description;
238
0
  unsigned code = 0;
239
240
0
  name = xmlGetProp(node, (const xmlChar *)XML_ATTR_NAME);
241
0
  code_str = xmlGetProp(node, (const xmlChar *)XML_ATTR_CODE);
242
0
  description = xmlGetProp(node, (const xmlChar *)XML_ATTR_DESCRIPTION);
243
244
0
  if (!name || !code_str) {
245
0
    xmlFree(name);
246
0
    xmlFree(code_str);
247
0
    xmlFree(description);
248
0
    return NULL;
249
0
  }
250
251
0
  sscanf((const char *)code_str, "%u", &code);
252
0
  xmlFree(code_str);
253
254
0
  enum_def = g_new0(dict_enum_def_t, 1);
255
0
  enum_def->value = name;  // Store enum name in value field
256
0
  enum_def->code = code;
257
0
  enum_def->description = description;
258
259
0
  return enum_def;
260
0
}
261
262
/* Forward declarations for mutual recursion */
263
// NOLINTNEXTLINE(misc-no-recursion)
264
static dict_field_def_t *process_field(xmlNodePtr node, const char *parent_path, bool is_array_element);
265
// NOLINTNEXTLINE(misc-no-recursion)
266
static dict_wildcardfield_def_t *process_wildcardfield(xmlNodePtr node, const char *parent_path);
267
268
// NOLINTNEXTLINE(misc-no-recursion)
269
static dict_wildcardfield_def_t *process_wildcardfield(xmlNodePtr node, const char *parent_path)
270
0
{
271
0
  xmlChar *name = xmlGetProp(node, (const xmlChar *)XML_ATTR_NAME);
272
0
  xmlChar *path = xmlGetProp(node, (const xmlChar *)XML_ATTR_PATH);
273
0
  xmlChar *display_value = xmlGetProp(node, (const xmlChar *)XML_ATTR_DISPLAY_VALUE);
274
0
  xmlChar *match = xmlGetProp(node, (const xmlChar *)XML_ATTR_MATCH);
275
276
0
  if (!path || !match) {
277
0
    ws_warning("JSON Dictionary: <wildcardfield> missing required 'path' or 'match' attribute");
278
0
    if (name) xmlFree(name);
279
0
    if (path) xmlFree(path);
280
0
    if (display_value) xmlFree(display_value);
281
0
    if (match) xmlFree(match);
282
0
    return NULL;
283
0
  }
284
285
  /* Extract alias as last segment of path */
286
0
  const char *last_dot = strrchr((const char *)path, '.');
287
0
  xmlChar *alias = last_dot ?
288
0
    xmlStrdup((const xmlChar *)(last_dot + 1)) :
289
0
    xmlStrdup(path);
290
291
0
  dict_wildcardfield_def_t *wf = g_new0(dict_wildcardfield_def_t, 1);
292
0
  wf->name          = name ? name : xmlStrdup(path);
293
0
  wf->path          = path;
294
0
  wf->alias         = alias;
295
0
  wf->display_value = display_value ? display_value : xmlStrdup((const xmlChar *)"key");
296
0
  wf->match         = match;
297
0
  wf->child_fields  = NULL;
298
0
  wf->child_wildcards = NULL;
299
300
  /* Process child nodes */
301
0
  for (xmlNodePtr child = node->children; child != NULL; child = child->next) {
302
0
    if (child->type != XML_ELEMENT_NODE)
303
0
      continue;
304
305
0
    if (xmlStrcmp(child->name, (const xmlChar *)XML_ELEMENT_FIELD) == 0) {
306
0
      dict_field_def_t *cf = process_field(child, parent_path, false);
307
0
      if (cf)
308
0
        wf->child_fields = g_slist_append(wf->child_fields, cf);
309
0
    }
310
0
    else if (xmlStrcmp(child->name, (const xmlChar *)XML_ELEMENT_ARRAY_ELEMENT) == 0) {
311
      /* Treat like process_field does: iterate <field> children of <array-element> */
312
0
      xmlChar *elem_type = xmlGetProp(child, (const xmlChar *)XML_ATTR_TYPE);
313
0
      for (xmlNodePtr ec = child->children; ec != NULL; ec = ec->next) {
314
0
        if (ec->type != XML_ELEMENT_NODE) continue;
315
0
        if (xmlStrcmp(ec->name, (const xmlChar *)XML_ELEMENT_FIELD) == 0) {
316
0
          dict_field_def_t *ef = process_field(ec, parent_path, true);
317
0
          if (ef)
318
0
            wf->child_fields = g_slist_append(wf->child_fields, ef);
319
0
        }
320
0
      }
321
0
      if (elem_type) xmlFree(elem_type);
322
0
    }
323
0
    else if (xmlStrcmp(child->name, (const xmlChar *)XML_ELEMENT_WILDCARDFIELD) == 0) {
324
0
      dict_wildcardfield_def_t *cwf = process_wildcardfield(child, parent_path);
325
0
      if (cwf)
326
0
        wf->child_wildcards = g_slist_append(wf->child_wildcards, cwf);
327
0
    }
328
0
  }
329
330
0
  return wf;
331
0
}
332
333
// Process field element (recursive for nested fields)
334
// NOLINTNEXTLINE(misc-no-recursion)
335
static dict_field_def_t *process_field(xmlNodePtr node, const char *parent_path, bool is_array_element)
336
0
{
337
0
  dict_field_def_t *field_def;
338
0
  xmlChar *name, *path, *type, *description, *info_attr, *df_attr;
339
0
  xmlChar *parser_attr, *parser_args_attr, *case_attr;
340
0
  xmlNodePtr child;
341
342
0
  name = xmlGetProp(node, (const xmlChar *)XML_ATTR_NAME);
343
0
  path = xmlGetProp(node, (const xmlChar *)XML_ATTR_PATH);
344
345
  // Path is required
346
0
  if (!path) {
347
    // If no path specified, try to construct from parent + name
348
0
    if (name && parent_path) {
349
0
      if (is_array_element) {
350
0
        path = xmlStrdup((const xmlChar *)wmem_strdup_printf(
351
0
          wmem_epan_scope(), "%s[].%s", parent_path, name));
352
0
      } else {
353
0
        path = xmlStrdup((const xmlChar *)wmem_strdup_printf(
354
0
          wmem_epan_scope(), "%s.%s", parent_path, name));
355
0
      }
356
0
    } else if (name) {
357
0
      path = xmlStrdup(name);
358
0
    } else {
359
0
      return NULL;
360
0
    }
361
0
  }
362
363
0
  type = xmlGetProp(node, (const xmlChar *)XML_ATTR_TYPE);
364
0
  description = xmlGetProp(node, (const xmlChar *)XML_ATTR_DESCRIPTION);
365
0
  info_attr = xmlGetProp(node, (const xmlChar *)XML_ATTR_INFO);
366
0
  df_attr = xmlGetProp(node, (const xmlChar *)XML_ATTR_DF);
367
0
  parser_attr = xmlGetProp(node, (const xmlChar *)XML_ATTR_PARSER);
368
0
  parser_args_attr = xmlGetProp(node, (const xmlChar *)XML_ATTR_PARSER_ARGS);
369
0
  case_attr = xmlGetProp(node, (const xmlChar *)XML_ATTR_CASE);
370
371
0
  field_def = g_new0(dict_field_def_t, 1);
372
0
  field_def->name = name ? name : xmlStrdup(path);
373
0
  field_def->path = path;
374
0
  field_def->type = type;
375
0
  field_def->description = description;
376
0
  field_def->is_array_element = is_array_element;
377
0
  field_def->child_fields = NULL;
378
0
  field_def->enum_values = NULL;
379
0
  field_def->info_label = info_attr;  // Store the info label string (NULL if not present)
380
0
  field_def->display_filter = df_attr;  // Store custom display filter name (NULL if not present)
381
0
  field_def->parser = parser_attr;  // parser  path
382
0
  field_def->parser_args = parser_args_attr;  // Additional parser arguments
383
0
  field_def->case_attr = case_attr;  // Store case sensitivity attribute
384
385
  /* Process child nodes */
386
0
  for (child = node->children; child != NULL; child = child->next) {
387
0
    if (child->type != XML_ELEMENT_NODE) {
388
0
      continue;
389
0
    }
390
391
0
    if (xmlStrcmp(child->name, (const xmlChar *)XML_ELEMENT_FIELD) == 0) {
392
      // Nested field
393
0
      dict_field_def_t *child_field = process_field(child,
394
0
        (const char *)path, false);
395
0
      if (child_field) {
396
0
        field_def->child_fields = g_slist_append(
397
0
          field_def->child_fields, child_field);
398
0
      }
399
0
    }
400
0
    else if (xmlStrcmp(child->name, (const xmlChar *)XML_ELEMENT_ARRAY_ELEMENT) == 0) {
401
      /* Array element definition */
402
0
      xmlChar *elem_type = xmlGetProp(child,
403
0
        (const xmlChar *)XML_ATTR_TYPE);
404
0
      xmlChar *elem_info = xmlGetProp(child,
405
0
        (const xmlChar *)XML_ATTR_INFO);
406
0
      xmlNodePtr elem_child;
407
0
      bool has_child_fields = false;
408
409
      // Process fields within array element
410
0
      for (elem_child = child->children; elem_child != NULL;
411
0
           elem_child = elem_child->next) {
412
0
        if (elem_child->type != XML_ELEMENT_NODE) {
413
0
          continue;
414
0
        }
415
416
0
        if (xmlStrcmp(elem_child->name,
417
0
          (const xmlChar *)XML_ELEMENT_FIELD) == 0) {
418
0
          dict_field_def_t *elem_field = process_field(
419
0
            elem_child, (const char *)path, true);
420
0
          if (elem_field) {
421
0
            field_def->child_fields = g_slist_append(
422
0
              field_def->child_fields, elem_field);
423
0
            has_child_fields = true;
424
0
          }
425
0
        }
426
0
      }
427
428
      /* Register a field at <path>[] for primitive array elements so values
429
       * are filterable (e.g. json.allowedSscModes[] == "SSC_MODE_1"). */
430
0
      if (!has_child_fields) {
431
0
        dict_field_def_t *elem_field = g_new0(dict_field_def_t, 1);
432
0
        elem_field->name = xmlStrdup(field_def->name);
433
0
        elem_field->path = xmlStrdup((const xmlChar *)wmem_strdup_printf(
434
0
          wmem_epan_scope(), "%s[]", (const char *)path));
435
0
        elem_field->type = elem_type ? xmlStrdup(elem_type) : NULL;
436
0
        elem_field->description = xmlStrdup((const xmlChar *)"Array element");
437
0
        elem_field->is_array_element = true;
438
0
        elem_field->child_fields = NULL;
439
0
        elem_field->enum_values = NULL;
440
0
        elem_field->info_label = xmlStrdup(elem_info);
441
442
0
        field_def->child_fields = g_slist_append(
443
0
          field_def->child_fields, elem_field);
444
0
      }
445
446
0
      xmlFree(elem_type);
447
0
      if (elem_info) xmlFree(elem_info);
448
0
    }
449
0
    else if (xmlStrcmp(child->name, (const xmlChar *)XML_ELEMENT_ENUM) == 0) {
450
      // Enum value
451
0
      dict_enum_def_t *enum_def = process_enum(child);
452
0
      if (enum_def) {
453
0
        field_def->enum_values = g_slist_append(
454
0
          field_def->enum_values, enum_def);
455
0
      }
456
0
    }
457
0
    else if (xmlStrcmp(child->name, (const xmlChar *)XML_ELEMENT_WILDCARDFIELD) == 0) {
458
0
      dict_wildcardfield_def_t *wf = process_wildcardfield(child,
459
0
        (const char *)path);
460
0
      if (wf) {
461
0
        field_def->wildcard_children = g_slist_append(
462
0
          field_def->wildcard_children, wf);
463
0
      }
464
0
    }
465
0
  }
466
467
0
  return field_def;
468
0
}
469
470
// Process protocol element
471
static dict_protocol_def_t *
472
process_protocol(xmlNodePtr node)
473
0
{
474
0
  dict_protocol_def_t *proto_def;
475
0
  xmlChar *name, *display_name, *port_str, *transport;
476
0
  xmlNodePtr child;
477
0
  GSList *ports = NULL;
478
479
0
  name = xmlGetProp(node, (const xmlChar *)XML_ATTR_NAME);
480
0
  display_name = xmlGetProp(node, (const xmlChar *)XML_ATTR_DISPLAY_NAME);
481
0
  port_str = xmlGetProp(node, (const xmlChar *)XML_ATTR_PORT);
482
0
  transport = xmlGetProp(node, (const xmlChar *)XML_ATTR_TRANSPORT);
483
  /* path= and case= are optional. path is a regex matched against the
484
   * HTTP/2 :path pseudo-header for dispatch in 5G SBI / HTTP-multiplexed
485
   * deployments where one TCP port carries many JSON services. */
486
0
  xmlChar *path_attr = xmlGetProp(node, (const xmlChar *)XML_ATTR_PATH);
487
0
  xmlChar *case_attr = xmlGetProp(node, (const xmlChar *)XML_ATTR_CASE);
488
0
  xmlChar *condition_attr = xmlGetProp(node, (const xmlChar *)XML_ATTR_CONDITION);
489
490
  // Parse comma-separated port numbers
491
0
  if (port_str) {
492
0
    char *port_str_copy = g_strdup((const char *)port_str);
493
0
    char *token = strtok(port_str_copy, ",");
494
0
    while (token != NULL) {
495
0
      unsigned port = 0;
496
      // Trim leading/trailing whitespace
497
0
      while (*token == ' ' || *token == '\t') token++;
498
0
      if (sscanf(token, "%u", &port) == 1 && port > 0) {
499
0
        unsigned *port_ptr = g_new(unsigned, 1);
500
0
        *port_ptr = port;
501
0
        ports = g_slist_append(ports, port_ptr);
502
0
      }
503
0
      token = strtok(NULL, ",");
504
0
    }
505
0
    g_free(port_str_copy);
506
0
    xmlFree(port_str);
507
0
  }
508
509
0
  proto_def = g_new0(dict_protocol_def_t, 1);
510
0
  proto_def->name = name;
511
0
  proto_def->display_name = display_name;
512
0
  proto_def->ports = ports;
513
0
  proto_def->transport = transport;
514
0
  proto_def->content_types = NULL;
515
0
  proto_def->fields = NULL;
516
0
  proto_def->path = path_attr;
517
0
  proto_def->case_attr = case_attr;
518
0
  proto_def->condition_attr = condition_attr;
519
520
  /* Process child nodes */
521
0
  for (child = node->children; child != NULL; child = child->next) {
522
0
    if (child->type != XML_ELEMENT_NODE) {
523
0
      continue;
524
0
    }
525
526
0
    if (xmlStrcmp(child->name, (const xmlChar *)XML_ELEMENT_CONTENT_TYPE) == 0) {
527
      // Content-type
528
0
      xmlChar *content_type = xmlNodeGetContent(child);
529
0
      if (content_type) {
530
0
        proto_def->content_types = g_slist_append(
531
0
          proto_def->content_types, content_type);
532
0
      }
533
0
    }
534
0
  }
535
536
0
  return proto_def;
537
0
}
538
539
// Parse display type hint from string
540
static json_display_type_t
541
parse_display_type(const char *display_str)
542
0
{
543
0
  if (!display_str) {
544
0
    return JSON_DISPLAY_NONE;
545
0
  }
546
547
0
  if (strcmp(display_str, "ipv4") == 0) {
548
0
    return JSON_DISPLAY_IPV4;
549
0
  } else if (strcmp(display_str, "ipv6") == 0) {
550
0
    return JSON_DISPLAY_IPV6;
551
0
  } else if (strcmp(display_str, "ether") == 0) {
552
0
    return JSON_DISPLAY_ETHER;
553
0
  } else if (strcmp(display_str, "absolute_time") == 0) {
554
0
    return JSON_DISPLAY_ABSOLUTE_TIME;
555
0
  } else if (strcmp(display_str, "relative_time") == 0) {
556
0
    return JSON_DISPLAY_RELATIVE_TIME;
557
0
  } else if (strcmp(display_str, "hex2dec") == 0) {
558
0
    return JSON_DISPLAY_HEX2DEC;
559
0
  }
560
561
0
  return JSON_DISPLAY_NONE;
562
0
}
563
564
/* Forward declaration for mutual recursion between create_wildcardfield and create_json_field */
565
// NOLINTNEXTLINE(misc-no-recursion)
566
static json_field_t *create_json_field(dict_field_def_t *field_def, wmem_array_t *hf_array,
567
      GPtrArray *ett_array, json_dictionary_t *dict, GHashTable *type_definitions);
568
569
// NOLINTNEXTLINE(misc-no-recursion)
570
static json_wildcard_field_t *create_wildcardfield(dict_wildcardfield_def_t *wf_def, wmem_array_t *hf_array,
571
         GPtrArray *ett_array, json_dictionary_t *dict, GHashTable *type_definitions)
572
0
{
573
0
  if (!wf_def || !wf_def->match)
574
0
    return NULL;
575
576
  /* Compile the match regex */
577
0
  char *regex_err = NULL;
578
0
  struct _ws_regex *re = ws_regex_compile((const char *)wf_def->match, &regex_err);
579
0
  if (!re) {
580
0
    ws_warning("JSON Dictionary: wildcardfield match='%s' regex compile failed: %s",
581
0
         (const char *)wf_def->match, regex_err ? regex_err : "unknown");
582
0
    g_free(regex_err);
583
0
    return NULL;
584
0
  }
585
586
0
  json_wildcard_field_t *jwf = wmem_new0(wmem_epan_scope(), json_wildcard_field_t);
587
0
  jwf->name          = wmem_strdup(wmem_epan_scope(), (const char *)wf_def->name);
588
0
  jwf->path          = wmem_strdup(wmem_epan_scope(), (const char *)wf_def->path);
589
0
  jwf->alias         = wmem_strdup(wmem_epan_scope(), (const char *)wf_def->alias);
590
0
  jwf->display_value = wmem_strdup(wmem_epan_scope(),
591
0
    wf_def->display_value ? (const char *)wf_def->display_value : "key");
592
0
  jwf->match_re      = re;
593
0
  jwf->hf_key_value  = -1;
594
  /* Track regex for cleanup at shutdown */
595
0
  dict->wildcard_regexes = g_slist_append(dict->wildcard_regexes, re);
596
0
  jwf->child_fields  = NULL;
597
0
  jwf->child_wildcards = NULL;
598
599
  /* Register the auto key-value string field: json.<path>.<displayvalue> */
600
0
  char *kv_filter = wmem_strdup_printf(wmem_epan_scope(), "json.%s.%s",
601
0
    jwf->path, jwf->display_value);
602
  /* Sanitize: strip brackets, collapse leading dots */
603
0
  {
604
0
    char *rp = kv_filter, *wp = kv_filter;
605
0
    while (*rp) {
606
0
      if (*rp != '[' && *rp != ']') *wp++ = *rp;
607
0
      rp++;
608
0
    }
609
0
    *wp = '\0';
610
0
    char *suffix = kv_filter + strlen("json.");
611
0
    while (*suffix == '.') memmove(suffix, suffix + 1, strlen(suffix + 1) + 1);
612
0
  }
613
614
0
  int *ett_ptr = wmem_new(wmem_epan_scope(), int);
615
0
  *ett_ptr = -1;
616
0
  jwf->ett = ett_ptr;
617
0
  g_ptr_array_add(ett_array, ett_ptr);
618
619
0
  hf_register_info kv_hf = {
620
0
    &jwf->hf_key_value,
621
0
    { jwf->display_value, kv_filter,
622
0
      FT_STRING, BASE_NONE, NULL, 0, "", HFILL }
623
0
  };
624
0
  wmem_array_append_one(hf_array, kv_hf);
625
626
  /* Register child <field> entries into jwf->child_fields tree */
627
0
  if (wf_def->child_fields) {
628
0
    jwf->child_fields = wmem_tree_new(wmem_epan_scope());
629
0
    for (GSList *elem = wf_def->child_fields; elem; elem = elem->next) {
630
0
      dict_field_def_t *cf = (dict_field_def_t *)elem->data;
631
0
      json_field_t *jf = create_json_field(cf, hf_array, ett_array, dict, type_definitions);
632
0
      if (jf)
633
0
        wmem_tree_insert_string(jwf->child_fields, jf->path, jf, 0);
634
0
    }
635
0
  }
636
637
  /* Register nested <wildcardfield> entries */
638
0
  for (GSList *wc = wf_def->child_wildcards; wc; wc = wc->next) {
639
0
    json_wildcard_field_t *cwf = create_wildcardfield(
640
0
      (dict_wildcardfield_def_t *)wc->data, hf_array, ett_array, dict, type_definitions);
641
0
    if (cwf)
642
0
      jwf->child_wildcards = g_slist_append(jwf->child_wildcards, cwf);
643
0
  }
644
645
0
  return jwf;
646
0
}
647
648
// Create json_field_t from dict_field_def_t and register header fields
649
// NOLINTNEXTLINE(misc-no-recursion)
650
static json_field_t *create_json_field(dict_field_def_t *field_def, wmem_array_t *hf_array,
651
      GPtrArray *ett_array, json_dictionary_t *dict, GHashTable *type_definitions)
652
0
{
653
0
  json_field_t *field;
654
0
  const base_type_mapping_t *base_type;
655
0
  const char *type_name;
656
0
  hf_register_info hf;
657
0
  int *ett_ptr;
658
0
  char *filter_name;
659
0
  json_display_type_t display_type = JSON_DISPLAY_NONE;
660
661
0
  if (!field_def) {
662
0
    return NULL;
663
0
  }
664
665
  // Resolve type
666
0
  type_name = (const char *)field_def->type;
667
0
  if (!type_name) {
668
0
    type_name = "string";  // Default
669
0
  }
670
671
  // Look up type in custom type definitions first
672
0
  dict_type_def_t *type_def = NULL;
673
0
  if (type_definitions) {
674
0
    type_def = g_hash_table_lookup(type_definitions, type_name);
675
0
  }
676
677
0
  if (type_def) {
678
0
    type_name = (const char *)type_def->base_type;
679
    /* Check for display hint in type definition */
680
0
    if (type_def->display) {
681
0
      display_type = parse_display_type((const char *)type_def->display);
682
0
    }
683
0
  }
684
685
0
  base_type = lookup_base_type(type_name);
686
687
  // Override field type for special displays
688
0
  enum ftenum ft_type = base_type->ft_type;
689
0
  int display_base = base_type->display_base;
690
691
  // Handle special display types first
692
0
  if (display_type == JSON_DISPLAY_IPV4) {
693
0
    ft_type = FT_IPv4;
694
0
    display_base = BASE_NONE;
695
0
  } else if (display_type == JSON_DISPLAY_IPV6) {
696
0
    ft_type = FT_IPv6;
697
0
    display_base = BASE_NONE;
698
0
  } else if (display_type == JSON_DISPLAY_ETHER) {
699
0
    ft_type = FT_ETHER;
700
0
    display_base = BASE_NONE;
701
0
  } else if (display_type == JSON_DISPLAY_ABSOLUTE_TIME) {
702
0
    ft_type = FT_ABSOLUTE_TIME;
703
0
    display_base = ABSOLUTE_TIME_LOCAL;
704
0
  } else if (display_type == JSON_DISPLAY_RELATIVE_TIME) {
705
0
    ft_type = FT_RELATIVE_TIME;
706
0
    display_base = BASE_NONE;
707
0
  } else if (display_type == JSON_DISPLAY_HEX2DEC) {
708
0
    ft_type = FT_UINT64;
709
0
    display_base = BASE_DEC_HEX;
710
0
  } else if (field_def->enum_values) {
711
    /* If field has enums and no special display type, use 32bit integer type
712
     * (value_string only supports 32-bit) */
713
0
    if (ft_type == FT_INT64) {
714
0
      ft_type = FT_INT32;
715
0
    } else if (ft_type == FT_UINT64) {
716
0
      ft_type = FT_UINT32;
717
0
    }
718
0
  }
719
720
  // If hex2dec has enums, downgrade to 32-bit
721
0
  if (display_type == JSON_DISPLAY_HEX2DEC && field_def->enum_values) {
722
0
    ft_type = FT_UINT32;
723
0
  }
724
725
  /* Last-loaded wins. When the same path is defined by multiple
726
   * dictionary files, the later one overrides the earlier one (matches
727
   * the existing rule for <protocol> ports and the "Field Collision
728
   * Example" already documented in resources/protocols/json/config.txt).
729
   *
730
   * This is what lets a personal dictionary in ~/.config/wireshark/json/
731
   * override a system field of the same path: personal loads after
732
   * system, so wmem_tree_insert_string() below overwrites the mapping.
733
   *
734
   * Note: the prior field's hf registration stays in the protocol's hf
735
   * array (Wireshark does not support deregistering individual fields
736
   * after proto_register_field_array). The tree-level override is what
737
   * matters for dispatch; the orphaned hf entry is bounded by the
738
   * number of overridden fields and released with wmem_epan_scope at
739
   * program exit. */
740
741
  /* Create field structure */
742
0
  field = wmem_new0(wmem_epan_scope(), json_field_t);
743
744
  // Append ** to field name
745
0
  if (field_def->parser) {
746
0
    field->name = wmem_strdup_printf(wmem_epan_scope(), "%s**",
747
0
      (const char *)field_def->name);
748
0
  } else {
749
0
    field->name = wmem_strdup(wmem_epan_scope(), (const char *)field_def->name);
750
0
  }
751
752
0
  field->path = wmem_strdup(wmem_epan_scope(), (const char *)field_def->path);
753
0
  field->path_hash = 0;
754
0
  field->type = base_type->type;
755
0
  field->display_type = display_type;
756
0
  field->hf_value = -1;
757
0
  field->ett = NULL;
758
0
  field->enum_values = NULL;
759
0
  field->child_fields = NULL;
760
0
  field->type_data = NULL;
761
0
  field->info_label = field_def->info_label ?
762
0
    wmem_strdup(wmem_epan_scope(), (const char *)field_def->info_label) : NULL;
763
0
  field->parser = field_def->parser ?
764
0
    wmem_strdup(wmem_epan_scope(), (const char *)field_def->parser) : NULL;
765
0
  field->parser_args = field_def->parser_args ?
766
0
    wmem_strdup(wmem_epan_scope(), (const char *)field_def->parser_args) : NULL;
767
0
  field->parser_child_hf = NULL;  // Will be populated dynamically during parsing
768
769
  // Parse case attribute (default to case-sensitive)
770
0
  field->case_insensitive = false;
771
0
  if (field_def->case_attr) {
772
0
    if (g_ascii_strcasecmp((const char *)field_def->case_attr, "insensitive") == 0) {
773
0
      field->case_insensitive = true;
774
0
      dict->has_case_insensitive_fields = true;
775
0
    } else if (g_ascii_strcasecmp((const char *)field_def->case_attr, "sensitive") == 0) {
776
0
      field->case_insensitive = false;
777
0
    } else {
778
      // Invalid value - log warning and use default
779
0
      ws_warning("JSON Dictionary: Invalid case attribute value '%s' for field '%s', using 'sensitive'",
780
0
           (const char *)field_def->case_attr, field->path);
781
0
      field->case_insensitive = false;
782
0
    }
783
0
  }
784
785
  // Use custom display filter name if specified, otherwise use pathbased name
786
0
  if (field_def->display_filter) {
787
0
    filter_name = wmem_strdup_printf(wmem_epan_scope(), "json.%s",
788
0
      (const char *)field_def->display_filter);
789
0
  } else {
790
0
    filter_name = wmem_strdup_printf(wmem_epan_scope(), "json.%s",
791
0
      field->path);
792
793
    // Remove brackets from filter name for array notation
794
0
    char *read_ptr = filter_name;
795
0
    char *write_ptr = filter_name;
796
0
    while (*read_ptr) {
797
0
      if (*read_ptr != '[' && *read_ptr != ']') {
798
0
        *write_ptr++ = *read_ptr;
799
0
      }
800
0
      read_ptr++;
801
0
    }
802
0
    *write_ptr = '\0';
803
804
    // Bracket removal can leave invalid dots: "[]" -> "json." (trailing dot)
805
    // and "[].foo" -> "json..foo" (doubled dot). Collapse any dots immediately
806
    // after the "json." prefix so the filter name stays valid.
807
0
    char *suffix = filter_name + strlen("json.");
808
0
    while (*suffix == '.') {
809
0
      memmove(suffix, suffix + 1, strlen(suffix + 1) + 1);
810
0
    }
811
    // If the path was purely "[]", suffix is now empty — use the field name.
812
0
    if (*suffix == '\0') {
813
0
      filter_name = wmem_strdup_printf(wmem_epan_scope(), "json.%s",
814
0
        (const char *)field_def->name);
815
0
      for (char *p = filter_name + strlen("json."); *p; p++) {
816
0
        *p = (*p == ' ') ? '_' : g_ascii_tolower(*p);
817
0
      }
818
0
    }
819
820
    /* Array container fields (type=Array, FT_NONE) get a "_array" suffix so
821
     * their filter name doesn't collide with the auto-generated element field
822
     * at the same path (which gets the clean natural filter name). This prevents
823
     * a crash when "apply as filter" is used on the array container node. */
824
0
    if (field->type == JSON_FIELD_ARRAY && !field_def->is_array_element) {
825
0
      filter_name = wmem_strdup_printf(wmem_epan_scope(), "%s_array", filter_name);
826
0
    }
827
0
  }
828
829
  /* Build enum value_string array if present */
830
0
  if (field_def->enum_values) {
831
0
    int enum_count = g_slist_length(field_def->enum_values);
832
0
    value_string *vs = wmem_alloc_array(wmem_epan_scope(), value_string,
833
0
      enum_count + 1);
834
0
    int idx = 0;
835
836
0
    for (GSList *elem = field_def->enum_values; elem; elem = elem->next) {
837
0
      dict_enum_def_t *enum_def = (dict_enum_def_t *)elem->data;
838
0
      vs[idx].value = enum_def->code;
839
0
      vs[idx].strptr = wmem_strdup(wmem_epan_scope(),
840
0
        (const char *)enum_def->value);
841
0
      idx++;
842
0
    }
843
    // Terminator
844
0
    vs[enum_count].value = 0;
845
0
    vs[enum_count].strptr = NULL;
846
847
0
    field->enum_values = vs;
848
0
  }
849
850
0
  hf.p_id = &field->hf_value;
851
0
  hf.hfinfo.name = field->name;
852
0
  hf.hfinfo.abbrev = filter_name;
853
0
  hf.hfinfo.type = ft_type;
854
0
  hf.hfinfo.display = display_base;
855
0
  hf.hfinfo.strings = field->enum_values ? VALS(field->enum_values) : NULL;
856
0
  hf.hfinfo.bitmask = 0;
857
0
  hf.hfinfo.blurb = field_def->description ?
858
0
    wmem_strdup(wmem_epan_scope(), (const char *)field_def->description) :
859
0
    "";
860
0
  HFILL_INIT(hf);
861
862
0
  wmem_array_append_one(hf_array, hf);
863
864
  // Register subtree for objects and arrays
865
0
  if (field->type == JSON_FIELD_OBJECT || field->type == JSON_FIELD_ARRAY) {
866
0
    ett_ptr = wmem_new(wmem_epan_scope(), int);
867
0
    *ett_ptr = -1;
868
0
    field->ett = ett_ptr;  // Store pointer, not value
869
0
    g_ptr_array_add(ett_array, ett_ptr);
870
0
  }
871
872
  // Insert into dictionary using stringbased lookup to avoid hash collisions
873
0
  wmem_tree_insert_string(dict->fields, field->path, field, 0);
874
875
  // Process child fields recursively
876
0
  if (field_def->child_fields) {
877
0
    field->child_fields = wmem_tree_new(wmem_epan_scope());
878
879
0
    for (GSList *elem = field_def->child_fields; elem; elem = elem->next) {
880
0
      dict_field_def_t *child_def = (dict_field_def_t *)elem->data;
881
0
      json_field_t *child_field = create_json_field(child_def,
882
0
        hf_array, ett_array, dict, type_definitions);
883
0
      if (child_field) {
884
        // Insert into parent's child_fields tree for hierarchical access
885
0
        wmem_tree_insert_string(field->child_fields,
886
0
          child_field->path, child_field, 0);
887
0
      }
888
0
    }
889
0
  }
890
891
  /* Process <wildcardfield> children */
892
0
  if (field_def->wildcard_children) {
893
0
    for (GSList *wc = field_def->wildcard_children; wc; wc = wc->next) {
894
0
      json_wildcard_field_t *jwf = create_wildcardfield(
895
0
        (dict_wildcardfield_def_t *)wc->data,
896
0
        hf_array, ett_array, dict, type_definitions);
897
0
      if (jwf)
898
0
        field->wildcard_fields = g_slist_append(field->wildcard_fields, jwf);
899
0
    }
900
0
  }
901
902
0
  return field;
903
0
}
904
905
/* Forward declaration — mutually recursive with populate_protocol_field_trees. */
906
// NOLINTNEXTLINE(misc-no-recursion)
907
static void populate_protocol_wildcard_trees(dict_wildcardfield_def_t *wdef, GSList *proto_list,
908
               json_dictionary_t *dict);
909
910
/* Recursively walk a field_def tree and insert each already-created
911
 * json_field_t* (fetched from dict->fields) into every protocol's scoped
912
 * field tree.  Also recurses into wildcard_children so alias-path fields
913
 * (e.g. dnnConfigurations.apn.5gQosProfile) are included.
914
 * Called once per file after all json_field_t objects have been created.
915
 * Shares pointers — no duplication. */
916
// NOLINTNEXTLINE(misc-no-recursion)
917
static void populate_protocol_field_trees(dict_field_def_t *fdef, GSList *proto_list,
918
            json_dictionary_t *dict)
919
0
{
920
0
  if (!fdef || !fdef->path)
921
0
    return;
922
0
  json_field_t *jf = (json_field_t *)wmem_tree_lookup_string(
923
0
    dict->fields, (const char *)fdef->path, 0);
924
0
  if (jf) {
925
0
    for (GSList *pe = proto_list; pe; pe = pe->next) {
926
0
      json_protocol_t *proto = (json_protocol_t *)pe->data;
927
0
      wmem_tree_insert_string(proto->fields, jf->path, jf, 0);
928
0
      if (jf->case_insensitive)
929
0
        proto->has_case_insensitive_fields = true;
930
0
    }
931
0
  }
932
0
  for (GSList *ce = fdef->child_fields; ce; ce = ce->next)
933
0
    populate_protocol_field_trees(
934
0
      (dict_field_def_t *)ce->data, proto_list, dict);
935
0
  for (GSList *wc = fdef->wildcard_children; wc; wc = wc->next)
936
0
    populate_protocol_wildcard_trees(
937
0
      (dict_wildcardfield_def_t *)wc->data, proto_list, dict);
938
0
}
939
940
/* Recurse into a wildcardfield def's child_fields and child_wildcards,
941
 * inserting their already-created json_field_t* into every protocol tree. */
942
// NOLINTNEXTLINE(misc-no-recursion)
943
static void populate_protocol_wildcard_trees(dict_wildcardfield_def_t *wdef,
944
         GSList *proto_list,
945
         json_dictionary_t *dict)
946
0
{
947
0
  if (!wdef)
948
0
    return;
949
0
  for (GSList *ce = wdef->child_fields; ce; ce = ce->next)
950
0
    populate_protocol_field_trees(
951
0
      (dict_field_def_t *)ce->data, proto_list, dict);
952
0
  for (GSList *wc = wdef->child_wildcards; wc; wc = wc->next)
953
0
    populate_protocol_wildcard_trees(
954
0
      (dict_wildcardfield_def_t *)wc->data, proto_list, dict);
955
0
}
956
957
/* Sanitize a protocol name= value into a valid Wireshark filter suffix:
958
 * lowercase, spaces→'_', non-[a-z0-9_]→'_', collapse runs of '_',
959
 * strip trailing '_', prefix '_' if result starts with a digit. */
960
static char *
961
sanitize_protocol_filter_name(const char *name)
962
0
{
963
0
  if (!name || *name == '\0')
964
0
    return NULL;
965
966
0
  size_t len = strlen(name);
967
0
  char *buf = (char *)wmem_alloc(wmem_epan_scope(), len + 2); /* +2: possible '_' prefix + NUL */
968
0
  size_t out = 0;
969
970
0
  for (size_t i = 0; i < len; i++) {
971
0
    unsigned char c = (unsigned char)name[i];
972
0
    char mapped;
973
0
    if (c >= 'A' && c <= 'Z')
974
0
      mapped = (char)(c + ('a' - 'A'));
975
0
    else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
976
0
      mapped = (char)c;
977
0
    else
978
0
      mapped = '_';
979
980
    /* Collapse consecutive underscores */
981
0
    if (mapped == '_' && out > 0 && buf[out - 1] == '_')
982
0
      continue;
983
0
    buf[out++] = mapped;
984
0
  }
985
986
  /* Strip trailing underscores */
987
0
  while (out > 0 && buf[out - 1] == '_')
988
0
    out--;
989
990
0
  buf[out] = '\0';
991
992
0
  if (out == 0)
993
0
    return NULL;
994
995
  /* Prefix '_' if first char is a digit */
996
0
  if (buf[0] >= '0' && buf[0] <= '9') {
997
0
    memmove(buf + 1, buf, out + 1);
998
0
    buf[0] = '_';
999
0
  }
1000
1001
0
  return buf;
1002
0
}
1003
1004
// Parse dictionary XML file
1005
static bool
1006
parse_dictionary_file(const char *filename, wmem_array_t *hf_array,
1007
          GPtrArray *ett_array, json_dictionary_t *dict)
1008
0
{
1009
0
  xmlDocPtr doc;
1010
0
  xmlNodePtr root, node, child;
1011
0
  GSList *field_defs = NULL;
1012
0
  GSList *protocol_defs = NULL;
1013
0
  bool success = false;
1014
0
  GHashTable *type_definitions = NULL;  /* Local to this dictionary file */
1015
1016
  // Initialize dictionary fields tree if not already done
1017
0
  if (!dict->fields) {
1018
0
    dict->fields = wmem_tree_new(wmem_epan_scope());
1019
0
  }
1020
1021
  // Initialize dictionary protocols tree if not already done
1022
0
  if (!dict->protocols) {
1023
0
    dict->protocols = wmem_tree_new(wmem_epan_scope());
1024
0
  }
1025
1026
  // Parse XML file
1027
0
  doc = xmlReadFile(filename, NULL, 0);
1028
0
  if (!doc) {
1029
0
    report_failure("JSON Dictionary: Could not parse file: %s\n", filename);
1030
0
    return false;
1031
0
  }
1032
1033
  /* Get root element */
1034
0
  root = xmlDocGetRootElement(doc);
1035
0
  if (!root) {
1036
0
    report_failure("JSON Dictionary: Empty document: %s\n", filename);
1037
0
    xmlFreeDoc(doc);
1038
0
    return false;
1039
0
  }
1040
1041
  // Check root element name
1042
0
  if (xmlStrcmp(root->name, (const xmlChar *)XML_ELEMENT_DICTIONARY) != 0) {
1043
0
    report_failure("JSON Dictionary: Root element is not '%s': %s\n",
1044
0
      XML_ELEMENT_DICTIONARY, filename);
1045
0
    xmlFreeDoc(doc);
1046
0
    return false;
1047
0
  }
1048
1049
  // Find <base> element
1050
0
  for (node = root->children; node != NULL; node = node->next) {
1051
0
    if (node->type != XML_ELEMENT_NODE) {
1052
0
      continue;
1053
0
    }
1054
1055
0
    if (xmlStrcmp(node->name, (const xmlChar *)XML_ELEMENT_BASE) != 0) {
1056
0
      continue;
1057
0
    }
1058
1059
    // Process elements within <base>
1060
0
    for (child = node->children; child != NULL; child = child->next) {
1061
0
      if (child->type != XML_ELEMENT_NODE) {
1062
0
        continue;
1063
0
      }
1064
1065
0
      if (xmlStrcmp(child->name, (const xmlChar *)XML_ELEMENT_TYPEDEFN) == 0) {
1066
        // Type definition
1067
0
        dict_type_def_t *type_def = process_typedefn(child);
1068
0
        if (type_def) {
1069
0
          if (!type_definitions) {
1070
0
            type_definitions = g_hash_table_new_full(
1071
0
              g_str_hash, g_str_equal,
1072
0
              NULL, (GDestroyNotify)dict_type_def_free);
1073
0
          }
1074
0
          g_hash_table_insert(type_definitions,
1075
0
            (void *)type_def->type_name, type_def);
1076
0
        }
1077
0
      }
1078
0
      else if (xmlStrcmp(child->name, (const xmlChar *)XML_ELEMENT_PROTOCOL) == 0) {
1079
        /* Protocol definition */
1080
0
        dict_protocol_def_t *proto_def = process_protocol(child);
1081
0
        if (proto_def) {
1082
0
          protocol_defs = g_slist_append(protocol_defs, proto_def);
1083
0
        }
1084
0
      }
1085
0
      else if (xmlStrcmp(child->name, (const xmlChar *)XML_ELEMENT_FIELD) == 0) {
1086
        /* Field definition */
1087
0
        dict_field_def_t *field_def = process_field(child, NULL, false);
1088
0
        if (field_def) {
1089
0
          field_defs = g_slist_append(field_defs, field_def);
1090
0
        }
1091
0
      }
1092
0
    }
1093
1094
0
    break;  // Only process first <base> element
1095
0
  }
1096
1097
  /* Create json_field_t structures and register header fields */
1098
0
  for (GSList *elem = field_defs; elem; elem = elem->next) {
1099
0
    dict_field_def_t *field_def = (dict_field_def_t *)elem->data;
1100
0
    create_json_field(field_def, hf_array, ett_array, dict, type_definitions);
1101
0
  }
1102
1103
  // Process protocol definitions
1104
0
  for (GSList *elem = protocol_defs; elem; elem = elem->next) {
1105
0
    dict_protocol_def_t *proto_def = (dict_protocol_def_t *)elem->data;
1106
0
    json_protocol_t *proto;
1107
1108
0
    proto = wmem_new(wmem_epan_scope(), json_protocol_t);
1109
0
    proto->name = proto_def->name ? wmem_strdup(wmem_epan_scope(), (const char *)proto_def->name) : NULL;
1110
0
    proto->display_name = proto_def->display_name ? wmem_strdup(wmem_epan_scope(), (const char *)proto_def->display_name) : NULL;
1111
0
    proto->port = 0;
1112
0
    proto->transport = proto_def->transport ? wmem_strdup(wmem_epan_scope(), (const char *)proto_def->transport) : NULL;
1113
0
    proto->content_types = NULL;
1114
0
    proto->path_regex = NULL;
1115
0
    proto->path_case_sensitive = false;
1116
0
    proto->port_list = NULL;
1117
0
    proto->fields = NULL;
1118
0
    proto->has_case_insensitive_fields = false;
1119
0
    proto->hf_proto_present = -1;
1120
1121
    /* Register a filterable FT_BOOLEAN field "json.<sanitized-name>"
1122
     * so users can filter packets by matched protocol. */
1123
0
    if (proto->name) {
1124
0
      char *suffix = sanitize_protocol_filter_name(proto->name);
1125
0
      if (suffix) {
1126
0
        char *abbrev = wmem_strdup_printf(wmem_epan_scope(),
1127
0
                  "json.%s", suffix);
1128
0
        char *label = wmem_strdup_printf(wmem_epan_scope(),
1129
0
                 "JSON+ Protocol: %s", proto->name);
1130
0
        hf_register_info proto_hf = {
1131
0
          &proto->hf_proto_present,
1132
0
          { label, abbrev,
1133
0
            FT_BOOLEAN, BASE_NONE, NULL, 0,
1134
0
            "Set when this JSON+ protocol matched dispatch", HFILL }
1135
0
        };
1136
0
        wmem_array_append_one(hf_array, proto_hf);
1137
0
      }
1138
0
    }
1139
1140
    /* condition= defaults to "or"; require_all=true only when the user
1141
     * explicitly sets condition="and". */
1142
0
    proto->require_all =
1143
0
      (proto_def->condition_attr &&
1144
0
       xmlStrcasecmp(proto_def->condition_attr,
1145
0
               (const xmlChar *)"and") == 0);
1146
1147
    // Any protocol entry (even one with no port and a failed regex) signals
1148
    // "the user wants protocol-gated dispatch" → enable gating.
1149
0
    dict->has_any_protocol = true;
1150
1151
    // Track every protocol for shutdown cleanup of non-wmem fields.
1152
0
    dict->all_protocols = g_slist_prepend(dict->all_protocols, proto);
1153
1154
    // Store protocol in dictionary under all specified ports, and also
1155
    // record the port list on the protocol itself so dispatch can verify
1156
    // "is this protocol bound to port N?" in AND mode.
1157
0
    for (GSList *port_elem = proto_def->ports; port_elem; port_elem = port_elem->next) {
1158
0
      unsigned *port_ptr = (unsigned *)port_elem->data;
1159
0
      if (port_ptr && *port_ptr > 0) {
1160
0
        wmem_tree_insert32(dict->protocols, *port_ptr, proto);
1161
0
        unsigned *p = wmem_new(wmem_epan_scope(), unsigned);
1162
0
        *p = *port_ptr;
1163
0
        proto->port_list = g_slist_prepend(proto->port_list, p);
1164
0
      }
1165
0
    }
1166
1167
    // Compile path regex and register for HTTP/2 :path-based dispatch.
1168
    // path_regex is heap-allocated (not wmem) and is freed by
1169
    // json_dictionary_cleanup() at shutdown.
1170
0
    if (proto_def->path) {
1171
      /* Default to case-sensitive, matching the existing <field>
1172
       * behavior at lines 558-559 ("default to case-sensitive").
1173
       * Only an explicit case="insensitive" flips it. */
1174
0
      proto->path_case_sensitive =
1175
0
        !(proto_def->case_attr &&
1176
0
          xmlStrcasecmp(proto_def->case_attr,
1177
0
            (const xmlChar *)"insensitive") == 0);
1178
0
      unsigned re_flags = WS_REGEX_NEVER_UTF;
1179
0
      if (!proto->path_case_sensitive) {
1180
0
        re_flags |= WS_REGEX_CASELESS;
1181
0
      }
1182
0
      char *errmsg = NULL;
1183
0
      proto->path_regex = ws_regex_compile_ex(
1184
0
        (const char *)proto_def->path, -1, &errmsg, re_flags);
1185
0
      if (proto->path_regex) {
1186
0
        dict->path_matchers = g_slist_append(dict->path_matchers, proto);
1187
0
      } else {
1188
0
        ws_warning("JSON dictionary: failed to compile path regex "
1189
0
             "for protocol \"%s\" (pattern=\"%s\"): %s",
1190
0
             proto->name ? proto->name : "(unnamed)",
1191
0
             (const char *)proto_def->path,
1192
0
             errmsg ? errmsg : "unknown error");
1193
0
        g_free(errmsg);
1194
0
      }
1195
0
    }
1196
0
  }
1197
1198
  /* Populate per-protocol scoped field trees for this file.
1199
   * Only runs when the file defined both protocols and fields.
1200
   * Fields from files with no <protocol> stay global-only (strict isolation). */
1201
0
  if (protocol_defs && field_defs) {
1202
0
    int n_protos = g_slist_length(protocol_defs);
1203
0
    GSList *file_protocols = NULL;
1204
0
    GSList *ap = dict->all_protocols;
1205
0
    for (int i = 0; i < n_protos && ap; i++, ap = ap->next)
1206
0
      file_protocols = g_slist_prepend(file_protocols, ap->data);
1207
1208
0
    for (GSList *pe = file_protocols; pe; pe = pe->next) {
1209
0
      json_protocol_t *proto = (json_protocol_t *)pe->data;
1210
0
      proto->fields = wmem_tree_new(wmem_epan_scope());
1211
0
      proto->has_case_insensitive_fields = false;
1212
0
    }
1213
1214
0
    for (GSList *fe = field_defs; fe; fe = fe->next)
1215
0
      populate_protocol_field_trees(
1216
0
        (dict_field_def_t *)fe->data, file_protocols, dict);
1217
1218
0
    g_slist_free(file_protocols);
1219
0
  }
1220
1221
0
  success = true;
1222
1223
  // Cleanup
1224
0
  g_slist_free_full(field_defs, (GDestroyNotify)dict_field_def_free);
1225
0
  g_slist_free_full(protocol_defs, (GDestroyNotify)dict_protocol_def_free);
1226
1227
  // Free local type definitions
1228
0
  if (type_definitions) {
1229
0
    g_hash_table_destroy(type_definitions);
1230
0
  }
1231
1232
0
  xmlFreeDoc(doc);
1233
1234
0
  return success;
1235
0
}
1236
1237
// Load JSON dictionaries from a single directory: read config.txt and the
1238
// XML files it lists, falling back to jsonmain.xml when there is no
1239
// config.txt. Returns true if any file loaded (or if the dir was empty,
1240
// in which case the loader is operating in "generic mode" for that dir).
1241
// Returns false if the directory itself does not exist or could not be
1242
// resolved — callers can treat that as "nothing to load here".
1243
static bool
1244
load_json_dictionary_from_dir(const char *dict_dir,
1245
            wmem_array_t *hf_array, GPtrArray *ett_array,
1246
            json_dictionary_t *dict)
1247
16
{
1248
16
  char *config_file;
1249
16
  char *dict_file;
1250
16
  FILE *fp;
1251
16
  char line[1024];
1252
16
  bool success = false;
1253
16
  int loaded_count = 0;
1254
1255
16
  if (!dict_dir) {
1256
0
    return false;
1257
0
  }
1258
1259
  // Try to load config file
1260
16
  config_file = wmem_strdup_printf(NULL, "%s%c%s",
1261
16
    dict_dir, G_DIR_SEPARATOR, "config.txt");
1262
1263
16
  if (file_exists(config_file)) {
1264
0
    fp = ws_fopen(config_file, "r");
1265
0
    if (fp) {
1266
0
      while (fgets(line, sizeof(line), fp)) {
1267
0
        char *trimmed = line;
1268
0
        char *end;
1269
1270
        // Trim leading whitespace
1271
0
        while (*trimmed == ' ' || *trimmed == '\t' || *trimmed == '\r' || *trimmed == '\n') {
1272
0
          trimmed++;
1273
0
        }
1274
1275
        // Skip comments and empty lines
1276
0
        if (*trimmed == '#' || *trimmed == '\0') {
1277
0
          continue;
1278
0
        }
1279
1280
        // Trim trailing whitespace and newline
1281
0
        end = trimmed + strlen(trimmed) - 1;
1282
0
        while (end > trimmed && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) {
1283
0
          *end = '\0';
1284
0
          end--;
1285
0
        }
1286
1287
        // Skip if empty after trimming
1288
0
        if (*trimmed == '\0') {
1289
0
          continue;
1290
0
        }
1291
1292
        // Build full path to dictionary file
1293
0
        dict_file = wmem_strdup_printf(NULL, "%s%c%s",
1294
0
          dict_dir, G_DIR_SEPARATOR, trimmed);
1295
1296
0
        if (file_exists(dict_file)) {
1297
0
          if (parse_dictionary_file(dict_file, hf_array, ett_array, dict)) {
1298
0
            loaded_count++;
1299
0
            success = true;
1300
0
          } else {
1301
0
            ws_message("JSON Dictionary: Failed to parse %s", trimmed);
1302
0
          }
1303
0
        } else {
1304
0
          ws_message("JSON Dictionary: File not found: %s", trimmed);
1305
0
        }
1306
1307
0
        wmem_free(NULL, dict_file);
1308
0
      }
1309
1310
0
      fclose(fp);
1311
1312
0
      if (loaded_count == 0) {
1313
0
        ws_message("JSON Dictionary: No dictionary files loaded from %s (using generic mode)", dict_dir);
1314
0
        success = true;  // just means generic mode
1315
0
      }
1316
0
    } else {
1317
0
      ws_message("JSON Dictionary: Could not open config file: %s", config_file);
1318
0
    }
1319
16
  } else {
1320
    // No config file try jsonmain.xml
1321
16
    dict_file = wmem_strdup_printf(NULL, "%s%c%s",
1322
16
      dict_dir, G_DIR_SEPARATOR, "jsonmain.xml");
1323
1324
16
    if (file_exists(dict_file)) {
1325
0
      success = parse_dictionary_file(dict_file, hf_array, ett_array, dict);
1326
16
    } else {
1327
16
      ws_message("JSON Dictionary: No config.txt or jsonmain.xml in %s (using generic mode)", dict_dir);
1328
16
      success = true;  // generic mode
1329
16
    }
1330
1331
16
    wmem_free(NULL, dict_file);
1332
16
  }
1333
1334
16
  wmem_free(NULL, config_file);
1335
16
  return success;
1336
16
}
1337
1338
// Load JSON dictionary from XML files
1339
// Main entry point called during protocol registration.
1340
//
1341
// Reads from two directories:
1342
//   1. System data dir (e.g. /usr/share/wireshark/json/) — shipped with
1343
//      Wireshark, overwritten on install/upgrade.
1344
//   2. Personal config dir (e.g. ~/.config/wireshark/json/) — opt-in,
1345
//      created by the user, survives upgrades.
1346
//
1347
// The personal dir is loaded second so its entries take precedence on
1348
// collisions (last-loaded-wins for ports, via wmem_tree_insert32).
1349
// The personal dir is silently skipped when it does not exist.
1350
//
1351
// Mirrors the pattern used by packet-radius.c (see lines around 2802).
1352
bool
1353
load_json_dictionary(wmem_array_t *hf_array, GPtrArray *ett_array,
1354
         json_dictionary_t *dict)
1355
16
{
1356
16
  bool any_loaded = false;
1357
1358
  /* System dictionary directory. */
1359
16
  char *system_dir = get_datafile_path("json", NULL);
1360
16
  if (system_dir) {
1361
16
    any_loaded |= load_json_dictionary_from_dir(system_dir,
1362
16
      hf_array, ett_array, dict);
1363
16
    g_free(system_dir);
1364
16
  } else {
1365
0
    report_failure("JSON Dictionary: Could not get system dictionary directory\n");
1366
0
  }
1367
1368
  /* Personal dictionary directory. Opt-in — silently skipped when the
1369
   * user has not created it. */
1370
16
  char *personal_dir = get_persconffile_path("json", false, NULL);
1371
16
  if (personal_dir) {
1372
16
    if (file_exists(personal_dir)) {
1373
0
      any_loaded |= load_json_dictionary_from_dir(personal_dir,
1374
0
        hf_array, ett_array, dict);
1375
0
    }
1376
16
    g_free(personal_dir);
1377
16
  }
1378
1379
16
  return any_loaded;
1380
16
}
1381
1382
/*
1383
 * Editor modelines  -  https://www.wireshark.org/tools/modelines.html
1384
 *
1385
 * Local variables:
1386
 * c-basic-offset: 8
1387
 * tab-width: 8
1388
 * indent-tabs-mode: t
1389
 * End:
1390
 *
1391
 * vi: set shiftwidth=8 tabstop=8 noexpandtab:
1392
 * :indentSize=8:tabSize=8:noTabs=false:
1393
 */