/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, ®ex_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 | | */ |