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