1 /* packet-udpcp.c
2  *
3  * Routines for UDPCP packet dissection (UDP-based reliable communication protocol).
4  * Described in the Open Base Station Initiative Reference Point 1 Specification
5  * (see https://web.archive.org/web/20171206005927/http://www.obsai.com/specs/RP1%20Spec%20v2_1.pdf, Appendix A)
6  *
foo(int i,unsigned int u)7  * Wireshark - Network traffic analyzer
8  * By Gerald Combs <gerald@wireshark.org>
9  * Copyright 1998 Gerald Combs
10  *
11  *
12  * SPDX-License-Identifier: GPL-2.0-or-later
13  */
14 
15 /* TODO:
16  * - Check for expected Acks and link between Data and Ack frames
17  * - Calculate/verify Checksum field
18  * - Sequence number analysis, i.e.
19  *     - check next expected Msg Id
20  *     - flag out-of-order Fragment Number within a MsgId?
21  */
22 
23 #include "config.h"
24 
25 #include <epan/conversation.h>
26 #include <epan/reassemble.h>
27 #include <epan/expert.h>
28 #include <epan/prefs.h>
29 
30 void proto_register_udpcp(void);
31 
32 static int proto_udpcp = -1;
33 
34 static int hf_udpcp_checksum = -1;
35 static int hf_udpcp_msg_type = -1;
36 static int hf_udpcp_version = -1;
37 
38 static int hf_udpcp_packet_transfer_options = -1;
39 static int hf_udpcp_n = -1;
40 static int hf_udpcp_c = -1;
41 static int hf_udpcp_s = -1;
42 static int hf_udpcp_d = -1;
43 static int hf_udpcp_reserved = -1;
44 
45 static int hf_udpcp_fragment_amount = -1;
46 static int hf_udpcp_fragment_number = -1;
47 
48 static int hf_udpcp_message_id = -1;
49 static int hf_udpcp_message_data_length = -1;
50 
51 static int hf_udpcp_payload = -1;
52 
53 /* For reassembly */
54 static int hf_udpcp_fragments = -1;
55 static int hf_udpcp_fragment = -1;
56 static int hf_udpcp_fragment_overlap = -1;
57 static int hf_udpcp_fragment_overlap_conflict = -1;
58 static int hf_udpcp_fragment_multiple_tails = -1;
59 static int hf_udpcp_fragment_too_long_fragment = -1;
60 static int hf_udpcp_fragment_error = -1;
61 static int hf_udpcp_fragment_count = -1;
62 static int hf_udpcp_reassembled_in = -1;
63 static int hf_udpcp_reassembled_length = -1;
64 static int hf_udpcp_reassembled_data = -1;
65 
66 
67 /* Subtrees */
68 static gint ett_udpcp = -1;
69 static gint ett_udpcp_packet_transfer_options = -1;
70 static gint ett_udpcp_fragments = -1;
71 static gint ett_udpcp_fragment  = -1;
72 
73 static const fragment_items udpcp_frag_items = {
74   &ett_udpcp_fragment,
75   &ett_udpcp_fragments,
76   &hf_udpcp_fragments,
77   &hf_udpcp_fragment,
78   &hf_udpcp_fragment_overlap,
79   &hf_udpcp_fragment_overlap_conflict,
80   &hf_udpcp_fragment_multiple_tails,
81   &hf_udpcp_fragment_too_long_fragment,
82   &hf_udpcp_fragment_error,
83   &hf_udpcp_fragment_count,
84   &hf_udpcp_reassembled_in,
85   &hf_udpcp_reassembled_length,
86   &hf_udpcp_reassembled_data,
87   "UDPCP fragments"
88 };
89 
90 
91 static expert_field ei_udpcp_checksum_should_be_zero = EI_INIT;
92 static expert_field ei_udpcp_d_not_zero_for_data = EI_INIT;
93 static expert_field ei_udpcp_reserved_not_zero = EI_INIT;
94 static expert_field ei_udpcp_n_s_ack = EI_INIT;
95 static expert_field ei_udpcp_payload_wrong_size = EI_INIT;
96 
97 static dissector_handle_t udpcp_handle;
98 
99 
100 void proto_reg_handoff_udpcp (void);
101 
102 /* User definable values */
103 static range_t *global_udpcp_port_range = NULL;
104 
105 #define DATA_FORMAT 0x01
106 #define ACK_FORMAT  0x02
107 
108 
109 static const value_string msg_type_vals[] = {
110   { DATA_FORMAT,   "Data Packet" },
111   { ACK_FORMAT,    "Ack Packet" },
112   { 0,     NULL }
113 };
114 
115 
116 /* Reassembly table. */
117 static reassembly_table udpcp_reassembly_table;
118 
119 static gpointer udpcp_temporary_key(const packet_info *pinfo _U_, const guint32 id _U_, const void *data)
120 {
121     return (gpointer)data;
122 }
123 
124 static gpointer udpcp_persistent_key(const packet_info *pinfo _U_, const guint32 id _U_,
125                                      const void *data)
126 {
127     return (gpointer)data;
128 }
129 
130 static void udpcp_free_temporary_key(gpointer ptr _U_)
131 {
132 }
133 
134 static void udpcp_free_persistent_key(gpointer ptr _U_)
135 {
136 }
137 
138 static reassembly_table_functions udpcp_reassembly_table_functions =
139 {
140     g_direct_hash,
141     g_direct_equal,
142     udpcp_temporary_key,
143     udpcp_persistent_key,
144     udpcp_free_temporary_key,
145     udpcp_free_persistent_key
146 };
147 
148 
149 /**************************************************************************/
150 /* Preferences state                                                      */
151 /**************************************************************************/
152 
153 /* Reassemble by default */
154 static gboolean global_udpcp_reassemble = TRUE;
155 
156 /* By default do try to decode payload as XML/SOAP */
157 static gboolean global_udpcp_decode_payload_as_soap = TRUE;
158 
159 
160 static dissector_handle_t xml_handle;
161 
162 /******************************/
163 /* Main dissection function.  */
164 static int
165 dissect_udpcp(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* data _U_)
166 {
167     proto_tree *udpcp_tree;
168     proto_item *root_ti;
169     gint offset = 0;
170 
171     /* Must be at least 12 bytes */
172     if (tvb_reported_length(tvb) < 12) {
173         return 0;
174     }
175 
176     /* Must be Data or Ack format. */
177     guint32 msg_type = tvb_get_guint8(tvb, 4) >> 6;
178     if (msg_type != DATA_FORMAT && msg_type != ACK_FORMAT) {
179         return 0;
180     }
181 
182     /* Protocol column */
183     col_set_str(pinfo->cinfo, COL_PROTOCOL, "UDPCP");
184 
185     /* Protocol root */
186     root_ti = proto_tree_add_item(tree, proto_udpcp, tvb, offset, -1, ENC_NA);
187     udpcp_tree = proto_item_add_subtree(root_ti, ett_udpcp);
188 
189     /* Checksum */
190     guint32 checksum;
191     proto_item *checksum_ti = proto_tree_add_item_ret_uint(udpcp_tree, hf_udpcp_checksum, tvb, offset, 4, ENC_BIG_ENDIAN, &checksum);
192     offset += 4;
193 
194     /* Msg-type */
195     proto_tree_add_item_ret_uint(udpcp_tree, hf_udpcp_msg_type, tvb, offset, 1, ENC_BIG_ENDIAN, &msg_type);
196     col_add_str(pinfo->cinfo, COL_INFO,
197                 (msg_type == DATA_FORMAT) ? "[Data] " : "[Ack]  ");
198     proto_item_append_text(root_ti, (msg_type == DATA_FORMAT) ? " [Data]" : " [Ack]");
199 
200     /* Version */
201     proto_tree_add_item(udpcp_tree, hf_udpcp_version, tvb, offset, 1, ENC_BIG_ENDIAN);
202 
203 
204     /***************************/
205     /* Packet Transfer Options */
206     proto_item *packet_transfer_options_ti =
207             proto_tree_add_string_format(udpcp_tree, hf_udpcp_packet_transfer_options, tvb, offset, 2,
208                                          "", "Packet Transfer Options (");
209     proto_tree *packet_transfer_options_tree =
210             proto_item_add_subtree(packet_transfer_options_ti, ett_udpcp_packet_transfer_options);
211     guint32 n, c, s, d;
212 
213     /* N */
214     proto_tree_add_item_ret_uint(packet_transfer_options_tree, hf_udpcp_n, tvb, offset, 1, ENC_BIG_ENDIAN, &n);
215     if (n) {
216         proto_item_append_text(packet_transfer_options_ti, "N");
217     }
218 
219     /* C */
220     proto_tree_add_item_ret_uint(packet_transfer_options_tree, hf_udpcp_c, tvb, offset, 1, ENC_BIG_ENDIAN, &c);
221     if (c) {
222        proto_item_append_text(packet_transfer_options_ti, "C");
223     }
224     if (!c && checksum) {
225         /* Expert info warning that checksum should be 0 if !c */
226         expert_add_info(pinfo, checksum_ti, &ei_udpcp_checksum_should_be_zero);
227     }
228 
229     /* S */
230     proto_tree_add_item_ret_uint(packet_transfer_options_tree, hf_udpcp_s, tvb, offset, 1, ENC_BIG_ENDIAN, &s);
231     offset++;
232     if (s) {
233         proto_item_append_text(packet_transfer_options_ti, "S");
234     }
235 
236     /* D */
237     proto_item *d_ti = proto_tree_add_item_ret_uint(packet_transfer_options_tree, hf_udpcp_d, tvb, offset, 1, ENC_BIG_ENDIAN, &d);
238     if (d) {
239         proto_item_append_text(packet_transfer_options_ti, "D");
240     }
241     /* Expert info if D not zero for data */
242     if ((msg_type == DATA_FORMAT) && d) {
243         expert_add_info(pinfo, d_ti, &ei_udpcp_d_not_zero_for_data);
244     }
245 
246     /* Reserved */
247     guint32 reserved;
248     proto_item *reserved_ti = proto_tree_add_item_ret_uint(packet_transfer_options_tree, hf_udpcp_reserved, tvb, offset, 1, ENC_BIG_ENDIAN, &reserved);
249     offset++;
250     /* Expert info if reserved not 0 */
251     if (reserved) {
252         expert_add_info(pinfo, reserved_ti, &ei_udpcp_reserved_not_zero);
253     }
254 
255     proto_item_append_text(packet_transfer_options_ti, ")");
256     /*************************/
257 
258 
259     /* Fragment Amount & Fragment Number */
260     guint32 fragment_amount, fragment_number;
261     proto_tree_add_item_ret_uint(udpcp_tree, hf_udpcp_fragment_amount, tvb, offset, 1, ENC_BIG_ENDIAN, &fragment_amount);
262     offset++;
263     proto_tree_add_item_ret_uint(udpcp_tree, hf_udpcp_fragment_number, tvb, offset, 1, ENC_BIG_ENDIAN, &fragment_number);
264     offset++;
265 
266     /* Message ID & Message Data Length */
267     guint32 message_id;
268     proto_tree_add_item_ret_uint(udpcp_tree, hf_udpcp_message_id, tvb, offset, 2, ENC_BIG_ENDIAN, &message_id);
269     col_append_fstr(pinfo->cinfo, COL_INFO, " Msg_ID=%3u", message_id);
270     offset += 2;
271     guint32 data_length;
272     proto_tree_add_item_ret_uint(udpcp_tree, hf_udpcp_message_data_length, tvb, offset, 2, ENC_BIG_ENDIAN, &data_length);
273     offset += 2;
274 
275     if (msg_type == DATA_FORMAT) {
276         if (!data_length) {
277             /* This could just be a sync frame */
278             if (!message_id && !n && !s) {
279                 col_append_str(pinfo->cinfo, COL_INFO, "  [Sync]");
280             }
281             /* Nothing more to show here */
282             return offset;
283         }
284 
285         /* Show if/when this frame should be acknowledged */
286         if (!n && !s) {
287             proto_item_append_text(packet_transfer_options_ti, " (All packets ACKd)");
288         }
289         else if (!n && s) {
290             proto_item_append_text(packet_transfer_options_ti, " (Last fragment ACKd)");
291         }
292         if (n) {
293             proto_item_append_text(packet_transfer_options_ti, " (Not ACKd)");
294         }
295 
296         /* Show fragment numbering.  Ignore confusing 0-based fragment numbering.. */
297         col_append_fstr(pinfo->cinfo, COL_INFO, "  [Frag %u/%u]",
298                         fragment_number+1, fragment_amount);
299 
300         /* There is data */
301         if ((fragment_amount == 1) && (fragment_number == 0)) {
302             /* Not fragmented - show payload now */
303             proto_item *data_ti = proto_tree_add_item(udpcp_tree, hf_udpcp_payload, tvb, offset, -1, ENC_ASCII);
304             col_append_fstr(pinfo->cinfo, COL_INFO, "  Data (%u bytes)", data_length);
305 
306             /* Check length is as signalled */
307             if (data_length != (guint32)tvb_reported_length_remaining(tvb, offset)) {
308                 expert_add_info_format(pinfo, data_ti, &ei_udpcp_payload_wrong_size, "Data length field was %u but %u bytes found",
309                                        data_length, tvb_reported_length_remaining(tvb, offset));
310             }
311 
312             if (global_udpcp_decode_payload_as_soap) {
313                 /* Send to XML dissector */
314                 tvbuff_t *next_tvb = tvb_new_subset_remaining(tvb, offset);
315                 call_dissector_only(xml_handle, next_tvb, pinfo, tree, NULL);
316             }
317         }
318         else {
319             /* Fragmented */
320             if (global_udpcp_reassemble) {
321                 /* Reassembly */
322                 /* Set fragmented flag. */
323                 gboolean save_fragmented = pinfo->fragmented;
324                 pinfo->fragmented = TRUE;
325                 fragment_head *fh;
326                 guint frag_data_len = tvb_reported_length_remaining(tvb, offset);
327 
328                 /* Add this fragment into reassembly */
329                 fh = fragment_add_seq_check(&udpcp_reassembly_table, tvb, offset, pinfo,
330                                             message_id,                                    /* id */
331                                             GUINT_TO_POINTER(message_id),                  /* data */
332                                             fragment_number,                               /* frag_number */
333                                             frag_data_len,                                 /* frag_data_len */
334                                             (fragment_number < (fragment_amount-1))        /* more_frags */
335                                             );
336 
337                 gboolean update_col_info = TRUE;
338                 /* See if this completes an SDU */
339                 tvbuff_t *next_tvb = process_reassembled_data(tvb, offset, pinfo, "Reassembled UDPCP Payload",
340                                                               fh, &udpcp_frag_items,
341                                                               &update_col_info, udpcp_tree);
342                 if (next_tvb) {
343                     /* Have reassembled data */
344                     proto_item *data_ti = proto_tree_add_item(udpcp_tree, hf_udpcp_payload, next_tvb, 0, -1, ENC_ASCII);
345                     col_append_fstr(pinfo->cinfo, COL_INFO, "  Reassembled Data (%u bytes)", data_length);
346 
347                     /* Check length is as signalled */
348                     if (data_length != (guint32)tvb_reported_length_remaining(next_tvb, 0)) {
349                         expert_add_info_format(pinfo, data_ti, &ei_udpcp_payload_wrong_size, "Data length field was %u but %u bytes found (reassembled)",
350                                                data_length, tvb_reported_length_remaining(next_tvb, 0));
351                     }
352 
353                     if (global_udpcp_decode_payload_as_soap) {
354                         /* Send to XML dissector */
355                         call_dissector_only(xml_handle, next_tvb, pinfo, tree, NULL);
356                     }
357                 }
358 
359                 /* Restore fragmented flag */
360                 pinfo->fragmented = save_fragmented;
361             }
362         }
363     }
364     else if (msg_type == ACK_FORMAT) {
365         /* N and S should be set - complain if not */
366         if (!n || !s) {
367             expert_add_info(pinfo, packet_transfer_options_ti, &ei_udpcp_n_s_ack);
368         }
369 
370         if (d) {
371             /* Duplicate data detected */
372             proto_item_append_text(packet_transfer_options_ti, " (duplicate)");
373             col_append_str(pinfo->cinfo, COL_INFO, " (duplicate)");
374         }
375     }
376 
377     return offset;
378 }
379 
380 
381 void
382 proto_register_udpcp(void)
383 {
384   static hf_register_info hf[] = {
385     { &hf_udpcp_checksum,
386       { "Checksum", "udpcp.checksum", FT_UINT32, BASE_HEX,
387         NULL, 0x0, "Adler32 checksum", HFILL }},
388       { &hf_udpcp_msg_type,
389         { "Msg Type", "udpcp.msg-type", FT_UINT8, BASE_HEX,
390           VALS(msg_type_vals), 0xc0, NULL, HFILL }},
391       { &hf_udpcp_version,
392         { "Version", "udpcp.version", FT_UINT8, BASE_HEX,
393           NULL, 0x38, NULL, HFILL }},
394 
395       { &hf_udpcp_packet_transfer_options,
396         { "Packet Transport Options", "udpcp.pto", FT_STRING, BASE_NONE,
397         NULL, 0x0, NULL, HFILL }},
398       { &hf_udpcp_n,
399         { "N", "udpcp.n", FT_UINT8, BASE_HEX,
400           NULL, 0x04, "Along with S bit, indicates whether acknowledgements should be sent", HFILL }},
401       { &hf_udpcp_c,
402         { "C", "udpcp.c", FT_UINT8, BASE_HEX,
403           NULL, 0x02, "When set, the checksum should be valid", HFILL }},
404       { &hf_udpcp_s,
405         { "S", "udpcp.s", FT_UINT8, BASE_HEX,
406           NULL, 0x01, "Along with N bit, indicates whether acknowledgements should be sent", HFILL }},
407       { &hf_udpcp_d,
408         { "D", "udpcp.d", FT_UINT8, BASE_HEX,
409           NULL, 0x80, "For ACK, indicates duplicate ACK", HFILL }},
410       { &hf_udpcp_reserved,
411         { "Reserved", "udpcp.reserved", FT_UINT8, BASE_HEX,
412           NULL, 0x7f, "Shall be set to 0", HFILL }},
413 
414       { &hf_udpcp_fragment_amount,
415         { "Fragment Amount", "udpcp.fragment-amount", FT_UINT8, BASE_DEC,
416           NULL, 0x0, "Total number of fragments of a message", HFILL }},
417       { &hf_udpcp_fragment_number,
418         { "Fragment Number", "udpcp.fragment-number", FT_UINT8, BASE_DEC,
419           NULL, 0x0, "Fragment number of current packet within msg.  Starts at 0", HFILL }},
420 
421       { &hf_udpcp_message_id,
422         { "Message ID", "udpcp.message-id", FT_UINT16, BASE_DEC,
423           NULL, 0x0, NULL, HFILL }},
424       { &hf_udpcp_message_data_length,
425         { "Message Data Length", "udpcp.message-data-length", FT_UINT16, BASE_DEC,
426           NULL, 0x0, NULL, HFILL }},
427 
428       { &hf_udpcp_payload,
429         { "Payload", "udpcp.payload", FT_BYTES, BASE_SHOW_ASCII_PRINTABLE,
430           NULL, 0x0, "Complete or reassembled payload", HFILL }},
431 
432       /* Reassembly */
433       { &hf_udpcp_fragment,
434         { "Fragment", "udpcp.fragment", FT_FRAMENUM, BASE_NONE,
435           NULL, 0x0, NULL, HFILL }},
436       { &hf_udpcp_fragments,
437         { "Fragments", "udpcp.fragments", FT_BYTES, BASE_NONE,
438           NULL, 0x0, NULL, HFILL }},
439       { &hf_udpcp_fragment_overlap,
440         { "Fragment overlap", "udpcp.fragment.overlap", FT_BOOLEAN, BASE_NONE,
441           NULL, 0x0, "Fragment overlaps with other fragments", HFILL }},
442       { &hf_udpcp_fragment_overlap_conflict,
443         { "Conflicting data in fragment overlap", "udpcp.fragment.overlap.conflict",
444           FT_BOOLEAN, BASE_NONE, NULL, 0x0,
445           "Overlapping fragments contained conflicting data", HFILL }},
446       { &hf_udpcp_fragment_multiple_tails,
447         { "Multiple tail fragments found", "udpcp.fragment.multipletails",
448           FT_BOOLEAN, BASE_NONE, NULL, 0x0,
449           "Several tails were found when defragmenting the packet", HFILL }},
450       { &hf_udpcp_fragment_too_long_fragment,
451         { "Fragment too long", "udpcp.fragment.toolongfragment",
452           FT_BOOLEAN, BASE_NONE, NULL, 0x0,
453           "Fragment contained data past end of packet", HFILL }},
454       { &hf_udpcp_fragment_error,
455         { "Defragmentation error", "udpcp.fragment.error", FT_FRAMENUM, BASE_NONE,
456           NULL, 0x0, "Defragmentation error due to illegal fragments", HFILL }},
457       { &hf_udpcp_fragment_count,
458         { "Fragment count", "udpcp.fragment.count", FT_UINT32, BASE_DEC,
459           NULL, 0x0, NULL, HFILL }},
460       { &hf_udpcp_reassembled_in,
461         { "Reassembled payload in frame", "udpcp.reassembled_in", FT_FRAMENUM, BASE_NONE,
462         NULL, 0x0, "This payload packet is reassembled in this frame", HFILL }},
463       { &hf_udpcp_reassembled_length,
464         { "Reassembled payload length", "udpcp.reassembled.length", FT_UINT32, BASE_DEC,
465           NULL, 0x0, "The total length of the reassembled payload", HFILL }},
466       { &hf_udpcp_reassembled_data,
467         { "Reassembled data", "udpcp.reassembled.data", FT_BYTES, BASE_NONE,
468           NULL, 0x0, "The reassembled payload", HFILL }},
469     };
470 
471     static gint *ett[] = {
472         &ett_udpcp,
473         &ett_udpcp_packet_transfer_options,
474         &ett_udpcp_fragments,
475         &ett_udpcp_fragment
476     };
477 
478     static ei_register_info ei[] = {
479         { &ei_udpcp_checksum_should_be_zero, { "udpcp.checksum-not-zero", PI_CHECKSUM, PI_WARN, "Checksum should be zero if !C.", EXPFILL }},
480         { &ei_udpcp_d_not_zero_for_data,     { "udpcp.d-not-zero-data", PI_SEQUENCE, PI_ERROR, "D should be zero for data frames", EXPFILL }},
481         { &ei_udpcp_reserved_not_zero,       { "udpcp.reserved-not-zero", PI_MALFORMED, PI_WARN, "Reserved bits not zero", EXPFILL }},
482         { &ei_udpcp_n_s_ack,                 { "udpcp.n-s-set-ack", PI_MALFORMED, PI_ERROR, "N or S set for ACK frame", EXPFILL }},
483         { &ei_udpcp_payload_wrong_size,      { "udpcp.payload-wrong-size", PI_MALFORMED, PI_ERROR, "Payload seen does not match size field", EXPFILL }},
484 
485     };
486 
487     module_t *udpcp_module;
488     expert_module_t *expert_udpcp;
489 
490     proto_udpcp = proto_register_protocol("UDPCP", "UDPCP", "udpcp");
491     proto_register_field_array(proto_udpcp, hf, array_length(hf));
492     proto_register_subtree_array(ett, array_length(ett));
493     expert_udpcp = expert_register_protocol(proto_udpcp);
494     expert_register_field_array(expert_udpcp, ei, array_length(ei));
495 
496     udpcp_handle = register_dissector("udpcp", dissect_udpcp, proto_udpcp);
497 
498     /* Register reassembly table. */
499     reassembly_table_register(&udpcp_reassembly_table,
500                               &udpcp_reassembly_table_functions);
501 
502     /* Preferences */
503     udpcp_module = prefs_register_protocol(proto_udpcp, NULL);
504 
505     /* Payload reassembly */
506     prefs_register_bool_preference(udpcp_module, "attempt_reassembly",
507                                    "Reassemble payload",
508                                    "",
509                                    &global_udpcp_reassemble);
510 
511     /* Whether to try XML dissector on payload.
512      * TODO: are there any other payload types we might see? */
513     prefs_register_bool_preference(udpcp_module, "attempt_xml_decode",
514         "Call XML dissector for payload",
515         "",
516         &global_udpcp_decode_payload_as_soap);
517 }
518 
519 static void
520 apply_udpcp_prefs(void)
521 {
522     global_udpcp_port_range = prefs_get_range_value("udpcp", "udp.port");
523 }
524 
525 void
526 proto_reg_handoff_udpcp(void)
527 {
528     dissector_add_uint_range_with_preference("udp.port", "", udpcp_handle);
529     apply_udpcp_prefs();
530 
531     xml_handle = find_dissector("xml");
532 }
533 
534 /*
535  * Editor modelines  -  https://www.wireshark.org/tools/modelines.html
536  *
537  * Local variables:
538  * c-basic-offset: 4
539  * tab-width: 8
540  * indent-tabs-mode: nil
541  * End:
542  *
543  * vi: set shiftwidth=4 tabstop=8 expandtab:
544  * :indentSize=4:tabSize=8:noTabs=true:
545  */
546