Revert "drsuapi_dissect_element_DsGetNCChangesCtr6TS_ctr6 dissect_krb5_PAC_NDRHEADERBLOB"
[wireshark-sm.git] / tools / eti2wireshark.py
blobfe11d64411078a0da560d3bd9658711f11dc3987
1 #!/usr/bin/env python3
3 # Generate Wireshark Dissectors for electronic trading/market data
4 # protocols such as ETI/EOBI.
6 # Targets Wireshark 3.5 or later.
8 # SPDX-FileCopyrightText: © 2021 Georg Sauthoff <mail@gms.tf>
9 # SPDX-License-Identifier: GPL-2.0-or-later
12 import argparse
13 import re
14 import sys
15 import xml.etree.ElementTree as ET
18 # inlined from upstream's etimodel.py
20 import itertools
22 def get_max_sizes(st, dt):
23 h = {}
24 for name, e in dt.items():
25 v = e.get('size', '0')
26 h[name] = int(v)
27 for name, e in itertools.chain((i for i in st.items() if i[1].get('type') != 'Message'),
28 (i for i in st.items() if i[1].get('type') == 'Message')):
29 s = 0
30 for m in e:
31 x = h.get(m.get('type'), 0)
32 s += x * int(m.get('cardinality'))
33 h[name] = s
34 return h
36 def get_min_sizes(st, dt):
37 h = {}
38 for name, e in dt.items():
39 v = e.get('size', '0')
40 if e.get('variableSize') is None:
41 h[name] = int(v)
42 else:
43 h[name] = 0
44 for name, e in itertools.chain((i for i in st.items() if i[1].get('type') != 'Message'),
45 (i for i in st.items() if i[1].get('type') == 'Message')):
46 s = 0
47 for m in e:
48 x = h.get(m.get('type'), 0)
49 s += x * int(m.get('minCardinality', '1'))
50 h[name] = s
51 return h
53 # end # inlined from upstream's etimodel.py
56 def get_used_types(st):
57 xs = set(y.get('type') for _, x in st.items() for y in x)
58 return xs
60 def get_data_types(d):
61 r = d.getroot()
62 x = r.find('DataTypes')
63 h = {}
64 for e in x:
65 h[e.get('name')] = e
66 return h
68 def get_structs(d):
69 r = d.getroot()
70 x = r.find('Structures')
71 h = {}
72 for e in x:
73 h[e.get('name')] = e
74 return h
76 def get_templates(st):
77 ts = []
78 for k, v in st.items():
79 if v.get('type') == 'Message':
80 ts.append((int(v.get('numericID')), k))
81 ts.sort()
82 return ts
85 def gen_header(proto, desc, o=sys.stdout):
86 if proto.startswith('eti') or proto.startswith('xti'):
87 ph = '#include "packet-tcp.h" // tcp_dissect_pdus()'
88 else:
89 ph = '#include "packet-udp.h" // udp_dissect_pdus()'
90 print(f'''// auto-generated by Georg Sauthoff's eti2wireshark.py
92 /* packet-eti.c
93 * Routines for {proto.upper()} dissection
94 * Copyright 2021, Georg Sauthoff <mail@gms.tf>
96 * Wireshark - Network traffic analyzer
97 * By Gerald Combs <gerald@wireshark.org>
98 * Copyright 1998 Gerald Combs
100 * SPDX-License-Identifier: GPL-2.0-or-later
104 * The {desc} ({proto.upper()}) is an electronic trading protocol
105 * that is used by a few exchanges (Eurex, Xetra, ...).
107 * It's a Length-Tag based protocol consisting of mostly fix sized
108 * request/response messages.
110 * Links:
111 * https://en.wikipedia.org/wiki/List_of_electronic_trading_protocols#Europe
112 * https://github.com/gsauthof/python-eti#protocol-descriptions
113 * https://github.com/gsauthof/python-eti#protocol-introduction
117 #include <config.h>
120 #include <epan/packet.h> // Should be first Wireshark include (other than config.h)
121 {ph}
122 #include <epan/expert.h> // expert info
124 #include <inttypes.h>
125 #include <stdio.h> // snprintf()
128 /* Prototypes */
129 /* (Required to prevent [-Wmissing-prototypes] warnings */
130 void proto_reg_handoff_{proto}(void);
131 void proto_register_{proto}(void);
133 static dissector_handle_t {proto}_handle;
135 static int proto_{proto};
136 ''', file=o)
139 def name2ident(name):
140 ll = True
141 xs = []
142 for i, c in enumerate(name):
143 if c.isupper():
144 if i > 0 and ll:
145 xs.append('_')
146 xs.append(c.lower())
147 ll = False
148 else:
149 xs.append(c)
150 ll = True
151 return ''.join(xs)
153 def gen_enums(dt, ts, o=sys.stdout):
154 print('static const value_string template_id_vals[] = { // TemplateID', file=o)
155 min_tid, max_tid = ts[0][0], ts[-1][0]
156 xs = [None] * (max_tid - min_tid + 1)
157 for tid, name in ts:
158 xs[tid-min_tid] = name
159 for i, name in enumerate(xs):
160 if name is None:
161 print(f' {{ {min_tid + i}, "Unknown" }},', file=o)
162 else:
163 print(f' {{ {min_tid + i}, "{name}" }},', file=o)
164 print(''' { 0, NULL }
166 static value_string_ext template_id_vals_ext = VALUE_STRING_EXT_INIT(template_id_vals);''', file=o)
167 name2access = { 'TemplateID': '&template_id_vals_ext' }
169 dedup = {}
170 for name, e in dt.items():
171 vs = [ (x.get('value'), x.get('name')) for x in e.findall('ValidValue') ]
172 if not vs:
173 continue
174 if e.get('rootType') == 'String' and e.get('size') != '1':
175 continue
177 ident = name2ident(name)
179 nv = e.get('noValue')
180 ws = [ v[0] for v in vs ]
181 if nv not in ws:
182 if nv.startswith('0x0') and e.get('rootType') == 'String':
183 nv = '\0'
184 vs.append( (nv, 'NO_VALUE') )
186 if e.get('type') == 'int':
187 vs.sort(key = lambda x : int(x[0], 0))
188 else:
189 vs.sort(key = lambda x : ord(x[0]))
190 s = '-'.join(f'{v[0]}:{v[1]}' for v in vs)
191 x = dedup.get(s)
192 if x is None:
193 dedup[s] = name
194 else:
195 name2access[name] = name2access[x]
196 print(f'// {name} aliased by {x}', file=o)
197 continue
199 print(f'static const value_string {ident}_vals[] = {{ // {name}', file=o)
200 for i, v in enumerate(vs):
201 if e.get('rootType') == 'String':
202 k = f"'{v[0]}'" if ord(v[0]) != 0 else '0'
203 print(f''' {{ {k}, "{v[1]}" }},''', file=o)
204 else:
205 print(f' {{ {v[0]}, "{v[1]}" }},', file=o)
206 print(''' { 0, NULL }
207 };''', file=o)
209 if len(vs) > 7:
210 print(f'static value_string_ext {ident}_vals_ext = VALUE_STRING_EXT_INIT({ident}_vals);', file=o)
211 name2access[name] = f'&{ident}_vals_ext'
212 else:
213 name2access[name] = f'VALS({ident}_vals)'
215 return name2access
218 def get_fields(st, dt):
219 seen = {}
220 for name, e in st.items():
221 for m in e:
222 t = dt.get(m.get('type'))
223 if is_padding(t):
224 continue
225 if not (is_int(t) or is_fixed_string(t) or is_var_string(t)):
226 continue
227 name = m.get('name')
228 if name in seen:
229 if seen[name] != t:
230 raise RuntimeError(f'Mismatching type for: {name}')
231 else:
232 seen[name] = t
233 vs = list(seen.items())
234 vs.sort()
235 return vs
237 def gen_field_handles(st, dt, proto, o=sys.stdout):
238 print(f'''static expert_field ei_{proto}_counter_overflow;
239 static expert_field ei_{proto}_invalid_template;
240 static expert_field ei_{proto}_invalid_length;''', file=o)
241 if not proto.startswith('eobi'):
242 print(f'static expert_field ei_{proto}_unaligned;', file=o)
243 print(f'''static expert_field ei_{proto}_missing;
244 static expert_field ei_{proto}_overused;
245 ''', file=o)
247 vs = get_fields(st, dt)
248 print(f'static int hf_{proto}[{len(vs)}];', file=o)
249 print(f'''static int hf_{proto}_dscp_exec_summary;
250 static int hf_{proto}_dscp_improved;
251 static int hf_{proto}_dscp_widened;''', file=o)
252 print('enum Field_Handle_Index {', file=o)
253 for i, (name, _) in enumerate(vs):
254 c = ' ' if i == 0 else ','
255 print(f' {c} {name.upper()}_FH_IDX', file=o)
256 print('};', file=o)
258 def type2ft(t):
259 if is_timestamp_ns(t):
260 return 'FT_ABSOLUTE_TIME'
261 if is_dscp(t):
262 return 'FT_UINT8'
263 if is_int(t):
264 if t.get('rootType') == 'String':
265 return 'FT_CHAR'
266 u = 'U' if is_unsigned(t) else ''
267 if t.get('size') is None:
268 raise RuntimeError(f'None size: {t.get("name")}')
269 size = int(t.get('size')) * 8
270 return f'FT_{u}INT{size}'
271 if is_fixed_string(t) or is_var_string(t):
272 # NB: technically, ETI fixed-strings are blank-padded,
273 # unless they are marked NO_VALUE, in that case
274 # the first byte is zero, followed by unspecified content.
275 # Also, some fixed-strings are zero-terminated, where again
276 # the bytes following the terminator are unspecified.
277 return 'FT_STRINGZTRUNC'
278 raise RuntimeError('unexpected type')
280 def type2enc(t):
281 if is_timestamp_ns(t):
282 return 'ABSOLUTE_TIME_UTC'
283 if is_dscp(t):
284 return 'BASE_HEX'
285 if is_int(t):
286 if t.get('rootType') == 'String':
287 # NB: basically only used when enum and value is unknown
288 return 'BASE_HEX'
289 else:
290 return 'BASE_DEC'
291 if is_fixed_string(t) or is_var_string(t):
292 # previously 'STR_ASCII', which was removed upstream
293 # cf. 19dcb725b61e384f665ad4b955f3b78f63e626d9
294 return 'BASE_NONE'
295 raise RuntimeError('unexpected type')
297 def gen_field_info(st, dt, n2enum, proto='eti', o=sys.stdout):
298 print(' static hf_register_info hf[] ={', file=o)
299 vs = get_fields(st, dt)
300 for i, (name, t) in enumerate(vs):
301 c = ' ' if i == 0 else ','
302 ft = type2ft(t)
303 enc = type2enc(t)
304 if is_enum(t) and not is_dscp(t):
305 vals = n2enum[t.get('name')]
306 if vals.startswith('&'):
307 extra_enc = '| BASE_EXT_STRING'
308 else:
309 extra_enc = ''
310 else:
311 vals = 'NULL'
312 extra_enc = ''
313 print(f''' {c} {{ &hf_{proto}[{name.upper()}_FH_IDX],
314 {{ "{name}", "{proto}.{name.lower()}",
315 {ft}, {enc}{extra_enc}, {vals}, 0x0,
316 NULL, HFILL }}
317 }}''', file=o)
318 print(f''' , {{ &hf_{proto}_dscp_exec_summary,
319 {{ "DSCP_ExecSummary", "{proto}.dscp_execsummary",
320 FT_BOOLEAN, 8, NULL, 0x10,
321 NULL, HFILL }}
323 , {{ &hf_{proto}_dscp_improved,
324 {{ "DSCP_Improved", "{proto}.dscp_improved",
325 FT_BOOLEAN, 8, NULL, 0x20,
326 NULL, HFILL }}
328 , {{ &hf_{proto}_dscp_widened,
329 {{ "DSCP_Widened", "{proto}.dscp_widened",
330 FT_BOOLEAN, 8, NULL, 0x40,
331 NULL, HFILL }}
332 }}''', file=o)
333 print(' };', file=o)
336 def gen_subtree_handles(st, proto='eti', o=sys.stdout):
337 ns = [ name for name, e in st.items() if e.get('type') != 'Message' ]
338 ns.sort()
339 h = dict( (n, i) for i, n in enumerate(ns, 1) )
340 print(f'static int ett_{proto}[{len(ns) + 1}];', file=o)
341 print(f'static int ett_{proto}_dscp;', file=o)
342 return h
345 def gen_subtree_array(st, proto='eti', o=sys.stdout):
346 n = sum(1 for name, e in st.items() if e.get('type') != 'Message')
347 n += 1
348 s = ', '.join(f'&ett_{proto}[{i}]' for i in range(n))
349 print(f' static int * const ett[] = {{ {s}, &ett_{proto}_dscp }};', file=o)
352 def gen_fields_table(st, dt, sh, o=sys.stdout):
353 name2off = {}
354 off = 0
355 names = []
356 for name, e in st.items():
357 if e.get('type') == 'Message':
358 continue
359 if name.endswith('Comp'):
360 s = name[:-4]
361 name2off[name] = off
362 off += len(s) + 1
363 names.append(s)
364 s = '\\0'.join(names)
365 print(f' static const char struct_names[] = "{s}";', file=o)
367 xs = [ x for x in st.items() if x[1].get('type') != 'Message' ]
368 xs += [ x for x in st.items() if x[1].get('type') == 'Message' ]
369 print(' static const struct ETI_Field fields[] = {', file=o)
370 i = 0
371 fields2idx = {}
372 for name, e in xs:
373 fields2idx[name] = i
374 print(f' // {name}@{i}', file=o)
375 counters = {}
376 cnt = 0
377 for m in e:
378 t = dt.get(m.get('type'))
379 c = ' ' if i == 0 else ','
380 typ = ''
381 size = int(t.get('size')) if t is not None else 0
382 rep = ''
383 fh = f'{m.get("name").upper()}_FH_IDX'
384 if is_padding(t):
385 print(f' {c} {{ ETI_PADDING, 0, {size}, 0, 0 }}', file=o)
386 elif is_fixed_point(t):
387 if size != 8:
388 raise RuntimeError('only supporting 8 byte fixed point')
389 fraction = int(t.get('precision'))
390 if fraction > 16:
391 raise RuntimeError('unusual high precisio in fixed point')
392 print(f' {c} {{ ETI_FIXED_POINT, {fraction}, {size}, {fh}, 0 }}', file=o)
393 elif is_timestamp_ns(t):
394 if size != 8:
395 raise RuntimeError('only supporting timestamps')
396 print(f' {c} {{ ETI_TIMESTAMP_NS, 0, {size}, {fh}, 0 }}', file=o)
397 elif is_dscp(t):
398 print(f' {c} {{ ETI_DSCP, 0, {size}, {fh}, 0 }}', file=o)
399 elif is_int(t):
400 u = 'U' if is_unsigned(t) else ''
401 if t.get('rootType') == 'String':
402 typ = 'ETI_CHAR'
403 else:
404 typ = f'ETI_{u}INT'
405 if is_enum(t):
406 typ += '_ENUM'
407 if t.get('type') == 'Counter':
408 counters[m.get('name')] = cnt
409 suf = f' // <- counter@{cnt}'
410 if cnt > 7:
411 raise RuntimeError(f'too many counters in message: {name}')
412 rep = cnt
413 cnt += 1
414 if typ != 'ETI_UINT':
415 raise RuntimeError('only unsigned counters supported')
416 if size > 2:
417 raise RuntimeError('only smaller counters supported')
418 typ = 'ETI_COUNTER'
419 ett_idx = t.get('maxValue')
420 else:
421 rep = 0
422 suf = ''
423 ett_idx = 0
424 print(f' {c} {{ {typ}, {rep}, {size}, {fh}, {ett_idx} }}{suf}', file=o)
425 elif is_fixed_string(t):
426 print(f' {c} {{ ETI_STRING, 0, {size}, {fh}, 0 }}', file=o)
427 elif is_var_string(t):
428 k = m.get('counter')
429 x = counters[k]
430 print(f' {c} {{ ETI_VAR_STRING, {x}, {size}, {fh}, 0 }}', file=o)
431 else:
432 a = m.get('type')
433 fields_idx = fields2idx[a]
434 k = m.get('counter')
435 if k:
436 counter_off = counters[k]
437 typ = 'ETI_VAR_STRUCT'
438 else:
439 counter_off = 0
440 typ = 'ETI_STRUCT'
441 names_off = name2off[m.get('type')]
442 ett_idx = sh[a]
443 print(f' {c} {{ {typ}, {counter_off}, {names_off}, {fields_idx}, {ett_idx} }} // {m.get("name")}', file=o)
444 i += 1
445 print(' , { ETI_EOF, 0, 0, 0, 0 }', file=o)
446 i += 1
447 print(' };', file=o)
448 return fields2idx
450 def gen_template_table(min_templateid, n, ts, fields2idx, o=sys.stdout):
451 xs = [ '-1' ] * n
452 for tid, name in ts:
453 xs[tid - min_templateid] = f'{fields2idx[name]} /* {name} */'
454 s = '\n , '.join(xs)
455 print(f' static const int16_t tid2fidx[] = {{\n {s}\n }};', file=o)
457 def gen_sizes_table(min_templateid, n, st, dt, ts, proto, o=sys.stdout):
458 is_eobi = proto.startswith('eobi')
459 xs = [ '0' if is_eobi else '{ 0, 0}' ] * n
460 min_s = get_min_sizes(st, dt)
461 max_s = get_max_sizes(st, dt)
462 if is_eobi:
463 for tid, name in ts:
464 xs[tid - min_templateid] = f'{max_s[name]} /* {name} */'
465 else:
466 for tid, name in ts:
467 xs[tid - min_templateid] = f'{{ {min_s[name]}, {max_s[name]} }} /* {name} */'
468 s = '\n , '.join(xs)
469 if is_eobi:
470 print(f' static const uint32_t tid2size[] = {{\n {s}\n }};', file=o)
471 else:
472 print(f' static const uint32_t tid2size[{n}][2] = {{\n {s}\n }};', file=o)
475 # yes, usage attribute of single fields depends on the context
476 # otherwise, we could just put the information into the fields table
477 # Example: EOBI.PacketHeader.MessageHeader.MsgSeqNum is unused whereas
478 # it's required in the EOBI ExecutionSummary and other messages
479 def gen_usage_table(min_templateid, n, ts, ams, o=sys.stdout):
480 def map_usage(m):
481 x = m.get('usage')
482 if x == 'mandatory':
483 return 0
484 elif x == 'optional':
485 return 1
486 elif x == 'unused':
487 return 2
488 else:
489 raise RuntimeError(f'unknown usage value: {x}')
491 h = {}
492 i = 0
493 print(' static const unsigned char usages[] = {', file=o)
494 for am in ams:
495 name = am.get("name")
496 tid = int(am.get('numericID'))
497 print(f' // {name}', file=o)
498 h[tid] = i
499 for e in am:
500 if e.tag == 'Group':
501 print(f' //// {e.get("type")}', file=o)
502 for m in e:
503 if m.get('hidden') == 'true' or pad_re.match(m.get('name')):
504 continue
505 k = ' ' if i == 0 else ','
506 print(f' {k} {map_usage(m)} // {m.get("name")}#{i}', file=o)
507 i += 1
508 print(' ///', file=o)
509 else:
510 if e.get('hidden') == 'true' or pad_re.match(e.get('name')):
511 continue
512 k = ' ' if i == 0 else ','
513 print(f' {k} {map_usage(e)} // {e.get("name")}#{i}', file=o)
514 i += 1
516 # NB: the last element is a filler to simplify the out-of-bounds check
517 # (cf. the uidx DISSECTOR_ASSER_CMPUINIT() before the switch statement)
518 # when the ETI_EOF of the message whose usage information comes last
519 # is reached
520 print(' , 0 // filler', file=o)
521 print(' };', file=o)
522 xs = [ '-1' ] * n
523 t2n = dict(ts)
524 for tid, uidx in h.items():
525 name = t2n[tid]
526 xs[tid - min_templateid] = f'{uidx} /* {name} */'
527 s = '\n , '.join(xs)
528 print(f' static const int16_t tid2uidx[] = {{\n {s}\n }};', file=o)
531 def gen_dscp_table(proto, o=sys.stdout):
532 print(f''' static int * const dscp_bits[] = {{
533 &hf_{proto}_dscp_exec_summary,
534 &hf_{proto}_dscp_improved,
535 &hf_{proto}_dscp_widened,
536 NULL
537 }};''', file=o)
540 def mk_int_case(size, signed, proto):
541 signed_str = 'i' if signed else ''
542 unsigned_str = '' if signed else 'u'
543 fmt_str = 'i' if signed else 'u'
544 if size == 2:
545 size_str = 's'
546 elif size == 4:
547 size_str = 'l'
548 elif size == 8:
549 size_str = '64'
550 type_str = f'g{unsigned_str}int{size * 8}'
551 no_value_str = f'INT{size * 8}_MIN' if signed else f'UINT{size * 8}_MAX'
552 pt_size = '64' if size == 8 else ''
553 if signed:
554 hex_str = '0x80' + '00' * (size - 1)
555 else:
556 hex_str = '0x' + 'ff' * size
557 if size == 1:
558 fn = f'tvb_get_g{unsigned_str}int8'
559 else:
560 fn = f'tvb_get_letoh{signed_str}{size_str}'
561 s = f'''case {size}:
563 {type_str} x = {fn}(tvb, off);
564 if (x == {no_value_str}) {{
565 proto_item *e = proto_tree_add_{unsigned_str}int{pt_size}_format_value(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, x, "NO_VALUE ({hex_str})");
566 if (!usages[uidx])
567 expert_add_info_format(pinfo, e, &ei_{proto}_missing, "required value is missing");
568 }} else {{
569 proto_item *e = proto_tree_add_{unsigned_str}int{pt_size}_format_value(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, x, "%" PRI{fmt_str}{size * 8}, x);
570 if (usages[uidx] == 2)
571 expert_add_info_format(pinfo, e, &ei_{proto}_overused, "unused value is set");
574 break;'''
575 return s
578 def gen_dissect_structs(o=sys.stdout):
579 print('''
580 enum ETI_Type {
581 ETI_EOF,
582 ETI_PADDING,
583 ETI_UINT,
584 ETI_INT,
585 ETI_UINT_ENUM,
586 ETI_INT_ENUM,
587 ETI_COUNTER,
588 ETI_FIXED_POINT,
589 ETI_TIMESTAMP_NS,
590 ETI_CHAR,
591 ETI_STRING,
592 ETI_VAR_STRING,
593 ETI_STRUCT,
594 ETI_VAR_STRUCT,
595 ETI_DSCP
598 struct ETI_Field {
599 uint8_t type;
600 uint8_t counter_off; // offset into counter array
601 // if ETI_COUNTER => storage
602 // if ETI_VAR_STRING or ETI_VAR_STRUCT => load
603 // to get length or repeat count
604 // if ETI_FIXED_POINT: #fractional digits
605 uint16_t size; // or offset into struct_names if ETI_STRUCT/ETI_VAR_STRUCT
606 uint16_t field_handle_idx; // or index into fields array if ETI_STRUCT/ETI_VAR_STRUT
607 uint16_t ett_idx; // index into ett array if ETI_STRUCT/ETI_VAR_STRUCT
608 // or max value if ETI_COUNTER
610 ''', file=o)
612 def gen_dissect_fn(st, dt, ts, sh, ams, proto, o=sys.stdout):
613 if proto.startswith('eti') or proto.startswith('xti'):
614 bl_fn = 'tvb_get_letohl'
615 template_off = 4
616 else:
617 bl_fn = 'tvb_get_letohs'
618 template_off = 2
619 print(f'''/* This method dissects fully reassembled messages */
620 static int
621 dissect_{proto}_message(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_)
623 col_set_str(pinfo->cinfo, COL_PROTOCOL, "{proto.upper()}");
624 col_clear(pinfo->cinfo, COL_INFO);
625 uint16_t templateid = tvb_get_letohs(tvb, {template_off});
626 const char *template_str = val_to_str_ext(templateid, &template_id_vals_ext, "Unknown {proto.upper()} template: 0x%04x");
627 col_add_str(pinfo->cinfo, COL_INFO, template_str);
629 /* create display subtree for the protocol */
630 proto_item *ti = proto_tree_add_item(tree, proto_{proto}, tvb, 0, -1, ENC_NA);
631 uint32_t bodylen= {bl_fn}(tvb, 0);
632 proto_item_append_text(ti, ", %s (%" PRIu16 "), BodyLen: %u", template_str, templateid, bodylen);
633 proto_tree *root = proto_item_add_subtree(ti, ett_{proto}[0]);
634 ''', file=o)
636 min_templateid = ts[0][0]
637 max_templateid = ts[-1][0]
638 n = max_templateid - min_templateid + 1
640 fields2idx = gen_fields_table(st, dt, sh, o)
641 gen_template_table(min_templateid, n, ts, fields2idx, o)
642 gen_sizes_table(min_templateid, n, st, dt, ts, proto, o)
643 gen_usage_table(min_templateid, n, ts, ams, o)
644 gen_dscp_table(proto, o)
646 print(f''' if (templateid < {min_templateid} || templateid > {max_templateid}) {{
647 proto_tree_add_expert_format(root, pinfo, &ei_{proto}_invalid_template, tvb, {template_off}, 4,
648 "Template ID out of range: %" PRIu16, templateid);
649 return tvb_captured_length(tvb);
651 int fidx = tid2fidx[templateid - {min_templateid}];
652 if (fidx == -1) {{
653 proto_tree_add_expert_format(root, pinfo, &ei_{proto}_invalid_template, tvb, {template_off}, 4,
654 "Unallocated Template ID: %" PRIu16, templateid);
655 return tvb_captured_length(tvb);
656 }}''', file=o)
658 if proto.startswith('eobi'):
659 print(f''' if (bodylen != tid2size[templateid - {min_templateid}]) {{
660 proto_tree_add_expert_format(root, pinfo, &ei_{proto}_invalid_length, tvb, 0, {template_off},
661 "Unexpected BodyLen value of %" PRIu32 ", expected: %" PRIu32, bodylen, tid2size[templateid - {min_templateid}]);
662 }}''', file=o)
663 else:
664 print(f''' if (bodylen < tid2size[templateid - {min_templateid}][0] || bodylen > tid2size[templateid - {min_templateid}][1]) {{
665 if (tid2size[templateid - {min_templateid}][0] != tid2size[templateid - {min_templateid}][1])
666 proto_tree_add_expert_format(root, pinfo, &ei_{proto}_invalid_length, tvb, 0, {template_off},
667 "Unexpected BodyLen value of %" PRIu32 ", expected: %" PRIu32 "..%" PRIu32, bodylen, tid2size[templateid - {min_templateid}][0], tid2size[templateid - {min_templateid}][1]);
668 else
669 proto_tree_add_expert_format(root, pinfo, &ei_{proto}_invalid_length, tvb, 0, {template_off},
670 "Unexpected BodyLen value of %" PRIu32 ", expected: %" PRIu32, bodylen, tid2size[templateid - {min_templateid}][0]);
672 if (bodylen % 8)
673 proto_tree_add_expert_format(root, pinfo, &ei_{proto}_unaligned, tvb, 0, {template_off},
674 "BodyLen value of %" PRIu32 " is not divisible by 8", bodylen);
675 ''', file=o)
677 print(f''' int uidx = tid2uidx[templateid - {min_templateid}];
678 DISSECTOR_ASSERT_CMPINT(uidx, >=, 0);
679 DISSECTOR_ASSERT_CMPUINT(((size_t)uidx), <, array_length(usages));
680 ''', file=o)
682 print(f''' int old_fidx = 0;
683 int old_uidx = 0;
684 unsigned top = 1;
685 unsigned counter[8] = {{0}};
686 unsigned off = 0;
687 unsigned struct_off = 0;
688 unsigned repeats = 0;
689 proto_tree *t = root;
690 while (top) {{
691 DISSECTOR_ASSERT_CMPINT(fidx, >=, 0);
692 DISSECTOR_ASSERT_CMPUINT(((size_t)fidx), <, array_length(fields));
693 DISSECTOR_ASSERT_CMPINT(uidx, >=, 0);
694 DISSECTOR_ASSERT_CMPUINT(((size_t)uidx), <, array_length(usages));
696 switch (fields[fidx].type) {{
697 case ETI_EOF:
698 DISSECTOR_ASSERT_CMPUINT(top, >=, 1);
699 DISSECTOR_ASSERT_CMPUINT(top, <=, 2);
700 if (t != root)
701 proto_item_set_len(t, off - struct_off);
702 if (repeats) {{
703 --repeats;
704 fidx = fields[old_fidx].field_handle_idx;
705 uidx = old_uidx;
706 t = proto_tree_add_subtree(root, tvb, off, -1, ett_{proto}[fields[old_fidx].ett_idx], NULL, &struct_names[fields[old_fidx].size]);
707 struct_off = off;
708 }} else {{
709 fidx = old_fidx + 1;
710 t = root;
711 --top;
713 break;
714 case ETI_VAR_STRUCT:
715 case ETI_STRUCT:
716 DISSECTOR_ASSERT_CMPUINT(fields[fidx].counter_off, <, array_length(counter));
717 repeats = fields[fidx].type == ETI_VAR_STRUCT ? counter[fields[fidx].counter_off] : 1;
718 if (repeats) {{
719 --repeats;
720 t = proto_tree_add_subtree(root, tvb, off, -1, ett_{proto}[fields[fidx].ett_idx], NULL, &struct_names[fields[fidx].size]);
721 struct_off = off;
722 old_fidx = fidx;
723 old_uidx = uidx;
724 fidx = fields[fidx].field_handle_idx;
725 DISSECTOR_ASSERT_CMPUINT(top, ==, 1);
726 ++top;
727 }} else {{
728 ++fidx;
730 break;
731 case ETI_PADDING:
732 off += fields[fidx].size;
733 ++fidx;
734 break;
735 case ETI_CHAR:
736 proto_tree_add_item(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, ENC_ASCII);
737 off += fields[fidx].size;
738 ++fidx;
739 ++uidx;
740 break;
741 case ETI_STRING:
743 uint8_t c = tvb_get_uint8(tvb, off);
744 if (c)
745 proto_tree_add_item(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, ENC_ASCII);
746 else {{
747 proto_item *e = proto_tree_add_string(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, "NO_VALUE ('0x00...')");
748 if (!usages[uidx])
749 expert_add_info_format(pinfo, e, &ei_{proto}_missing, "required value is missing");
752 off += fields[fidx].size;
753 ++fidx;
754 ++uidx;
755 break;
756 case ETI_VAR_STRING:
757 DISSECTOR_ASSERT_CMPUINT(fields[fidx].counter_off, <, array_length(counter));
758 proto_tree_add_item(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, counter[fields[fidx].counter_off], ENC_ASCII);
759 off += counter[fields[fidx].counter_off];
760 ++fidx;
761 ++uidx;
762 break;
763 case ETI_COUNTER:
764 DISSECTOR_ASSERT_CMPUINT(fields[fidx].counter_off, <, array_length(counter));
765 DISSECTOR_ASSERT_CMPUINT(fields[fidx].size, <=, 2);
767 switch (fields[fidx].size) {{
768 case 1:
770 uint8_t x = tvb_get_uint8(tvb, off);
771 if (x == UINT8_MAX) {{
772 proto_tree_add_uint_format_value(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, x, "NO_VALUE (0xff)");
773 counter[fields[fidx].counter_off] = 0;
774 }} else {{
775 proto_item *e = proto_tree_add_uint_format_value(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, x, "%" PRIu8, x);
776 if (x > fields[fidx].ett_idx) {{
777 counter[fields[fidx].counter_off] = fields[fidx].ett_idx;
778 expert_add_info_format(pinfo, e, &ei_{proto}_counter_overflow, "Counter overflow: %" PRIu8 " > %" PRIu16, x, fields[fidx].ett_idx);
779 }} else {{
780 counter[fields[fidx].counter_off] = x;
784 break;
785 case 2:
787 uint16_t x = tvb_get_letohs(tvb, off);
788 if (x == UINT16_MAX) {{
789 proto_tree_add_uint_format_value(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, x, "NO_VALUE (0xffff)");
790 counter[fields[fidx].counter_off] = 0;
791 }} else {{
792 proto_item *e = proto_tree_add_uint_format_value(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, x, "%" PRIu16, x);
793 if (x > fields[fidx].ett_idx) {{
794 counter[fields[fidx].counter_off] = fields[fidx].ett_idx;
795 expert_add_info_format(pinfo, e, &ei_{proto}_counter_overflow, "Counter overflow: %" PRIu16 " > %" PRIu16, x, fields[fidx].ett_idx);
796 }} else {{
797 counter[fields[fidx].counter_off] = x;
801 break;
804 off += fields[fidx].size;
805 ++fidx;
806 ++uidx;
807 break;
808 case ETI_UINT:
809 switch (fields[fidx].size) {{
810 {mk_int_case(1, False, proto)}
811 {mk_int_case(2, False, proto)}
812 {mk_int_case(4, False, proto)}
813 {mk_int_case(8, False, proto)}
815 off += fields[fidx].size;
816 ++fidx;
817 ++uidx;
818 break;
819 case ETI_INT:
820 switch (fields[fidx].size) {{
821 {mk_int_case(1, True, proto)}
822 {mk_int_case(2, True, proto)}
823 {mk_int_case(4, True, proto)}
824 {mk_int_case(8, True, proto)}
826 off += fields[fidx].size;
827 ++fidx;
828 ++uidx;
829 break;
830 case ETI_UINT_ENUM:
831 case ETI_INT_ENUM:
832 proto_tree_add_item(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, ENC_LITTLE_ENDIAN);
833 off += fields[fidx].size;
834 ++fidx;
835 ++uidx;
836 break;
837 case ETI_FIXED_POINT:
838 DISSECTOR_ASSERT_CMPUINT(fields[fidx].size, ==, 8);
839 DISSECTOR_ASSERT_CMPUINT(fields[fidx].counter_off, >, 0);
840 DISSECTOR_ASSERT_CMPUINT(fields[fidx].counter_off, <=, 16);
842 int64_t x = tvb_get_letohi64(tvb, off);
843 if (x == INT64_MIN) {{
844 proto_item *e = proto_tree_add_int64_format_value(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, x, "NO_VALUE (0x8000000000000000)");
845 if (!usages[uidx])
846 expert_add_info_format(pinfo, e, &ei_{proto}_missing, "required value is missing");
847 }} else {{
848 unsigned slack = fields[fidx].counter_off + 1;
849 if (x < 0)
850 slack += 1;
851 char s[21];
852 int n = snprintf(s, sizeof s, "%0*" PRIi64, slack, x);
853 DISSECTOR_ASSERT_CMPUINT(n, >, 0);
854 unsigned k = n - fields[fidx].counter_off;
855 proto_tree_add_int64_format_value(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, x, "%.*s.%s", k, s, s + k);
858 off += fields[fidx].size;
859 ++fidx;
860 ++uidx;
861 break;
862 case ETI_TIMESTAMP_NS:
863 DISSECTOR_ASSERT_CMPUINT(fields[fidx].size, ==, 8);
864 proto_tree_add_item(t, hf_{proto}[fields[fidx].field_handle_idx], tvb, off, fields[fidx].size, ENC_LITTLE_ENDIAN | ENC_TIME_NSECS);
865 off += fields[fidx].size;
866 ++fidx;
867 ++uidx;
868 break;
869 case ETI_DSCP:
870 DISSECTOR_ASSERT_CMPUINT(fields[fidx].size, ==, 1);
871 proto_tree_add_bitmask(t, tvb, off, hf_{proto}[fields[fidx].field_handle_idx], ett_{proto}_dscp, dscp_bits, ENC_LITTLE_ENDIAN);
872 off += fields[fidx].size;
873 ++fidx;
874 ++uidx;
875 break;
878 ''', file=o)
880 print(''' return tvb_captured_length(tvb);
882 ''', file=o)
884 print(f'''/* determine PDU length of protocol {proto.upper()} */
885 static unsigned
886 get_{proto}_message_len(packet_info *pinfo _U_, tvbuff_t *tvb, int offset, void *data _U_)
888 return (unsigned){bl_fn}(tvb, offset);
890 ''', file=o)
892 if proto.startswith('eobi'):
893 print(f'''static int
894 dissect_{proto}(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree,
895 void *data)
897 return udp_dissect_pdus(tvb, pinfo, tree, 4, NULL,
898 get_{proto}_message_len, dissect_{proto}_message, data);
900 ''', file=o)
901 else:
902 print(f'''static int
903 dissect_{proto}(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree,
904 void *data)
906 tcp_dissect_pdus(tvb, pinfo, tree, true, 4 /* bytes to read for bodylen */,
907 get_{proto}_message_len, dissect_{proto}_message, data);
908 return tvb_captured_length(tvb);
910 ''', file=o)
912 def gen_register_fn(st, dt, n2enum, proto, desc, o=sys.stdout):
913 print(f'''void
914 proto_register_{proto}(void)
915 {{''', file=o)
916 gen_field_info(st, dt, n2enum, proto, o)
918 print(f''' static ei_register_info ei[] = {{
920 &ei_{proto}_counter_overflow,
921 {{ "{proto}.counter_overflow", PI_PROTOCOL, PI_WARN, "Counter Overflow", EXPFILL }}
924 &ei_{proto}_invalid_template,
925 {{ "{proto}.invalid_template", PI_PROTOCOL, PI_ERROR, "Invalid Template ID", EXPFILL }}
928 &ei_{proto}_invalid_length,
929 {{ "{proto}.invalid_length", PI_PROTOCOL, PI_ERROR, "Invalid Body Length", EXPFILL }}
930 }},''', file=o)
931 if not proto.startswith('eobi'):
932 print(f''' {{
933 &ei_{proto}_unaligned,
934 {{ "{proto}.unaligned", PI_PROTOCOL, PI_ERROR, "A Body Length not divisible by 8 leads to unaligned followup messages", EXPFILL }}
935 }},''', file=o)
936 print(f''' {{
937 &ei_{proto}_missing,
938 {{ "{proto}.missing", PI_PROTOCOL, PI_WARN, "A required value is missing", EXPFILL }}
941 &ei_{proto}_overused,
942 {{ "{proto}.overused", PI_PROTOCOL, PI_WARN, "An unused value is set", EXPFILL }}
944 }};''', file=o)
946 print(f''' proto_{proto} = proto_register_protocol("{desc}",
947 "{proto.upper()}", "{proto}");''', file=o)
949 print(f''' expert_module_t *expert_{proto} = expert_register_protocol(proto_{proto});
950 expert_register_field_array(expert_{proto}, ei, array_length(ei));''', file=o)
952 print(f' proto_register_field_array(proto_{proto}, hf, array_length(hf));',
953 file=o)
954 gen_subtree_array(st, proto, o)
955 print(' proto_register_subtree_array(ett, array_length(ett));', file=o)
956 if proto.startswith('eobi'):
957 print(f' proto_disable_by_default(proto_{proto});', file=o)
959 print(f'\n {proto}_handle = register_dissector("{proto}", dissect_{proto}, proto_{proto});', file=o)
960 print('}\n', file=o)
963 def gen_handoff_fn(proto, o=sys.stdout):
964 print(f'''void
965 proto_reg_handoff_{proto}(void)
967 // cf. N7 Network Access Guide, e.g.
968 // https://www.xetra.com/xetra-en/technology/t7/system-documentation/release10-0/Release-10.0-2692700?frag=2692724
969 // https://www.xetra.com/resource/blob/2762078/388b727972b5122945eedf0e63c36920/data/N7-Network-Access-Guide-v2.0.59.pdf
971 ''', file=o)
972 if proto.startswith('eti'):
973 print(f''' // NB: can only be called once for a port/handle pair ...
974 // dissector_add_uint_with_preference("tcp.port", 19006 /* LF PROD */, eti_handle);
976 dissector_add_uint("tcp.port", 19006 /* LF PROD */, {proto}_handle);
977 dissector_add_uint("tcp.port", 19043 /* PS PROD */, {proto}_handle);
978 dissector_add_uint("tcp.port", 19506 /* LF SIMU */, {proto}_handle);
979 dissector_add_uint("tcp.port", 19543 /* PS SIMU */, {proto}_handle);''', file=o)
980 elif proto.startswith('xti'):
981 print(f''' // NB: unfortunately, Cash-ETI shares the same ports as Derivatives-ETI ...
982 // We thus can't really add a well-know port for XTI.
983 // Use Wireshark's `Decode As...` or tshark's `-d tcp.port=19043,xti` feature
984 // to switch from ETI to XTI dissection.
985 dissector_add_uint_with_preference("tcp.port", 19042 /* dummy */, {proto}_handle);''', file=o)
986 else:
987 print(f''' static const int ports[] = {{
988 59000, // Snapshot EUREX US-allowed PROD
989 59001, // Incremental EUREX US-allowed PROD
990 59032, // Snapshot EUREX US-restricted PROD
991 59033, // Incremental EUREX US-restricted PROD
992 59500, // Snapshot EUREX US-allowed SIMU
993 59501, // Incremental EUREX US-allowed SIMU
994 59532, // Snapshot EUREX US-restricted SIMU
995 59533, // Incremental EUREX US-restricted SIMU
997 57000, // Snapshot FX US-allowed PROD
998 57001, // Incremental FX US-allowed PROD
999 57032, // Snapshot FX US-restricted PROD
1000 57033, // Incremental FX US-restricted PROD
1001 57500, // Snapshot FX US-allowed SIMU
1002 57501, // Incremental FX US-allowed SIMU
1003 57532, // Snapshot FX US-restricted SIMU
1004 57533, // Incremental FX US-restricted SIMU
1006 59000, // Snapshot Xetra PROD
1007 59001, // Incremental Xetra PROD
1008 59500, // Snapshot Xetra SIMU
1009 59501, // Incremental Xetra SIMU
1011 56000, // Snapshot Boerse Frankfurt PROD
1012 56001, // Incremental Boerse Frankfurt PROD
1013 56500, // Snapshot Boerse Frankfurt SIMU
1014 56501 // Incremental Boerse Frankfurt SIMU
1016 for (unsigned i = 0; i < array_length(ports); ++i)
1017 dissector_add_uint("udp.port", ports[i], {proto}_handle);''', file=o)
1018 print('}', file=o)
1020 def is_int(t):
1021 if t is not None:
1022 r = t.get('rootType')
1023 return r in ('int', 'floatDecimal') or (r == 'String' and t.get('size') == '1')
1024 return False
1026 def is_enum(t):
1027 if t is not None:
1028 r = t.get('rootType')
1029 if r == 'int' or (r == 'String' and t.get('size') == '1'):
1030 return t.find('ValidValue') is not None
1031 return False
1033 def is_fixed_point(t):
1034 return t is not None and t.get('rootType') == 'floatDecimal'
1036 def is_timestamp_ns(t):
1037 return t is not None and t.get('type') == 'UTCTimestamp'
1039 def is_dscp(t):
1040 return t is not None and t.get('name') == 'DSCP'
1042 pad_re = re.compile('Pad[1-9]')
1044 def is_padding(t):
1045 if t is not None:
1046 return t.get('rootType') == 'String' and pad_re.match(t.get('name'))
1047 return False
1049 def is_fixed_string(t):
1050 if t is not None:
1051 return t.get('rootType') in ('String', 'data') and not t.get('variableSize')
1052 return False
1054 def is_var_string(t):
1055 if t is not None:
1056 return t.get('rootType') in ('String', 'data') and t.get('variableSize') is not None
1057 return False
1059 def is_unsigned(t):
1060 v = t.get('minValue')
1061 return v is not None and not v.startswith('-')
1063 def is_counter(t):
1064 return t.get('type') == 'Counter'
1066 def type_to_fmt(t):
1067 if is_padding(t):
1068 return f'{t.get("size")}x'
1069 elif is_int(t):
1070 n = int(t.get('size'))
1071 if n == 1:
1072 return 'B'
1073 else:
1074 if n == 2:
1075 c = 'h'
1076 elif n == 4:
1077 c = 'i'
1078 elif n == 8:
1079 c = 'q'
1080 else:
1081 raise ValueError(f'unknown int size {n}')
1082 if is_unsigned(t):
1083 c = c.upper()
1084 return c
1085 elif is_fixed_string(t):
1086 return f'{t.get("size")}s'
1087 else:
1088 return '?'
1090 def pp_int_type(t):
1091 if not is_int(t):
1092 return None
1093 s = 'i'
1094 if is_unsigned(t):
1095 s = 'u'
1096 n = int(t.get('size'))
1097 s += str(n)
1098 return s
1100 def is_elementary(t):
1101 return t is not None and t.get('counter') is None
1103 def group_members(e, dt):
1104 xs = []
1105 ms = []
1106 for m in e:
1107 t = dt.get(m.get('type'))
1108 if is_elementary(t):
1109 ms.append(m)
1110 else:
1111 if ms:
1112 xs.append(ms)
1113 ms = []
1114 xs.append([m])
1115 if ms:
1116 xs.append(ms)
1117 return xs
1121 def parse_args():
1122 p = argparse.ArgumentParser(description='Generate Wireshark Dissector for ETI/EOBI style protocol specifications')
1123 p.add_argument('filename', help='protocol description XML file')
1124 p.add_argument('--proto', default='eti',
1125 help='short protocol name (default: %(default)s)')
1126 p.add_argument('--desc', '-d',
1127 default='Enhanced Trading Interface',
1128 help='protocol description (default: %(default)s)')
1129 p.add_argument('--output', '-o', default='-',
1130 help='output filename (default: stdout)')
1131 args = p.parse_args()
1132 return args
1134 def main():
1135 args = parse_args()
1136 filename = args.filename
1137 d = ET.parse(filename)
1138 o = sys.stdout if args.output == '-' else open(args.output, 'w')
1139 proto = args.proto
1141 version = (d.getroot().get('version'), d.getroot().get('subVersion'))
1142 desc = f'{args.desc} {version[0]}'
1144 dt = get_data_types(d)
1145 st = get_structs(d)
1146 used = get_used_types(st)
1147 for k in list(dt.keys()):
1148 if k not in used:
1149 del dt[k]
1150 ts = get_templates(st)
1151 ams = d.getroot().find('ApplicationMessages')
1153 gen_header(proto, desc, o)
1154 gen_field_handles(st, dt, proto, o)
1155 n2enum = gen_enums(dt, ts, o)
1156 gen_dissect_structs(o)
1157 sh = gen_subtree_handles(st, proto, o)
1158 gen_dissect_fn(st, dt, ts, sh, ams, proto, o)
1159 gen_register_fn(st, dt, n2enum, proto, desc, o)
1160 gen_handoff_fn(proto, o)
1163 if __name__ == '__main__':
1164 sys.exit(main())