5a71c9f69cd1df2f6ab02a26b195cde2977ba4dc
[metze/wireshark/wip.git] / ui / qt / endpoint_dialog.cpp
1 /* endpoint_dialog.cpp
2  *
3  * Wireshark - Network traffic analyzer
4  * By Gerald Combs <gerald@wireshark.org>
5  * Copyright 1998 Gerald Combs
6  *
7  * SPDX-License-Identifier: GPL-2.0+*/
8
9 #include "endpoint_dialog.h"
10
11 #ifdef HAVE_GEOIP
12 #include <GeoIP.h>
13 #include <epan/geoip_db.h>
14 #include <wsutil/pint.h>
15 #endif
16
17 #include <epan/prefs.h>
18
19 #include "ui/recent.h"
20 #include "ui/traffic_table_ui.h"
21
22 #include "wsutil/str_util.h"
23
24 #include <ui/qt/utils/qt_ui_utils.h>
25 #include "wireshark_application.h"
26
27 #include <QCheckBox>
28 #include <QDesktopServices>
29 #include <QDialogButtonBox>
30 #include <QMessageBox>
31 #include <QPushButton>
32 #include <QUrl>
33
34 static const QString table_name_ = QObject::tr("Endpoint");
35 EndpointDialog::EndpointDialog(QWidget &parent, CaptureFile &cf, int cli_proto_id, const char *filter) :
36     TrafficTableDialog(parent, cf, filter, table_name_)
37 {
38 #ifdef HAVE_GEOIP
39     map_bt_ = buttonBox()->addButton(tr("Map"), QDialogButtonBox::ActionRole);
40     map_bt_->setToolTip(tr("Draw IPv4 or IPv6 endpoints on a map."));
41     connect(map_bt_, SIGNAL(clicked()), this, SLOT(createMap()));
42
43     connect(trafficTableTabWidget(), SIGNAL(currentChanged(int)), this, SLOT(tabChanged()));
44 #endif
45
46     addProgressFrame(&parent);
47
48     QList<int> endp_protos;
49     for (GList *endp_tab = recent.endpoint_tabs; endp_tab; endp_tab = endp_tab->next) {
50         int proto_id = proto_get_id_by_short_name((const char *)endp_tab->data);
51         if (proto_id > -1 && !endp_protos.contains(proto_id)) {
52             endp_protos.append(proto_id);
53         }
54     }
55
56     if (endp_protos.isEmpty()) {
57         endp_protos = defaultProtos();
58     }
59
60     // Bring the command-line specified type to the front.
61     if ((cli_proto_id > 0) && (get_conversation_by_proto_id(cli_proto_id))) {
62         endp_protos.removeAll(cli_proto_id);
63         endp_protos.prepend(cli_proto_id);
64     }
65
66     // QTabWidget selects the first item by default.
67     foreach (int endp_proto, endp_protos) {
68         addTrafficTable(get_conversation_by_proto_id(endp_proto));
69     }
70
71     fillTypeMenu(endp_protos);
72
73 #ifdef HAVE_GEOIP
74     tabChanged();
75 #endif
76
77     QPushButton *close_bt = buttonBox()->button(QDialogButtonBox::Close);
78     if (close_bt) {
79         close_bt->setDefault(true);
80     }
81
82     updateWidgets();
83 //    currentTabChanged();
84
85     cap_file_.delayedRetapPackets();
86 }
87
88 EndpointDialog::~EndpointDialog()
89 {
90     prefs_clear_string_list(recent.endpoint_tabs);
91     recent.endpoint_tabs = NULL;
92
93     EndpointTreeWidget *cur_tree = qobject_cast<EndpointTreeWidget *>(trafficTableTabWidget()->currentWidget());
94     foreach (QAction *ea, traffic_type_menu_.actions()) {
95         int proto_id = ea->data().value<int>();
96         if (proto_id_to_tree_.contains(proto_id) && ea->isChecked()) {
97             char *title = g_strdup(proto_get_protocol_short_name(find_protocol_by_id(proto_id)));
98             if (proto_id_to_tree_[proto_id] == cur_tree) {
99                 recent.endpoint_tabs = g_list_prepend(recent.endpoint_tabs, title);
100             } else {
101                 recent.endpoint_tabs = g_list_append(recent.endpoint_tabs, title);
102             }
103         }
104     }
105 }
106
107 void EndpointDialog::captureFileClosing()
108 {
109     // Keep the dialog around but disable any controls that depend
110     // on a live capture file.
111     for (int i = 0; i < trafficTableTabWidget()->count(); i++) {
112         EndpointTreeWidget *cur_tree = qobject_cast<EndpointTreeWidget *>(trafficTableTabWidget()->widget(i));
113         disconnect(cur_tree, SIGNAL(filterAction(QString,FilterAction::Action,FilterAction::ActionType)),
114                    this, SIGNAL(filterAction(QString,FilterAction::Action,FilterAction::ActionType)));
115     }
116     displayFilterCheckBox()->setEnabled(false);
117     enabledTypesPushButton()->setEnabled(false);
118     TrafficTableDialog::captureFileClosing();
119 }
120
121 bool EndpointDialog::addTrafficTable(register_ct_t *table)
122 {
123     int proto_id = get_conversation_proto_id(table);
124
125     if (!table || proto_id_to_tree_.contains(proto_id)) {
126         return false;
127     }
128
129     EndpointTreeWidget *endp_tree = new EndpointTreeWidget(this, table);
130
131     proto_id_to_tree_[proto_id] = endp_tree;
132     const char* table_name = proto_get_protocol_short_name(find_protocol_by_id(proto_id));
133
134     trafficTableTabWidget()->addTab(endp_tree, table_name);
135
136     connect(endp_tree, SIGNAL(titleChanged(QWidget*,QString)),
137             this, SLOT(setTabText(QWidget*,QString)));
138     connect(endp_tree, SIGNAL(filterAction(QString,FilterAction::Action,FilterAction::ActionType)),
139             this, SIGNAL(filterAction(QString,FilterAction::Action,FilterAction::ActionType)));
140     connect(nameResolutionCheckBox(), SIGNAL(toggled(bool)),
141             endp_tree, SLOT(setNameResolutionEnabled(bool)));
142
143     // XXX Move to ConversationTreeWidget ctor?
144     QByteArray filter_utf8;
145     const char *filter = NULL;
146     if (displayFilterCheckBox()->isChecked()) {
147         filter = cap_file_.capFile()->dfilter;
148     } else if (!filter_.isEmpty()) {
149         filter_utf8 = filter_.toUtf8();
150         filter = filter_utf8.constData();
151     }
152
153     endp_tree->trafficTreeHash()->user_data = endp_tree;
154
155     registerTapListener(proto_get_protocol_filter_name(proto_id), endp_tree->trafficTreeHash(), filter, 0,
156                         EndpointTreeWidget::tapReset,
157                         get_hostlist_packet_func(table),
158                         EndpointTreeWidget::tapDraw);
159
160 #ifdef HAVE_GEOIP
161     connect(endp_tree, SIGNAL(geoIPStatusChanged()), this, SLOT(tabChanged()));
162 #endif
163     return true;
164 }
165
166 #ifdef HAVE_GEOIP
167 void EndpointDialog::tabChanged()
168 {
169     EndpointTreeWidget *cur_tree = qobject_cast<EndpointTreeWidget *>(trafficTableTabWidget()->currentWidget());
170     map_bt_->setEnabled(cur_tree && cur_tree->hasGeoIPData());
171 }
172
173 void EndpointDialog::createMap()
174 {
175     EndpointTreeWidget *cur_tree = qobject_cast<EndpointTreeWidget *>(trafficTableTabWidget()->currentWidget());
176     if (!cur_tree) {
177         return;
178     }
179
180     gchar *err_str;
181     gchar *map_path = create_endpoint_geoip_map(cur_tree->trafficTreeHash()->conv_array, &err_str);
182     if (!map_path) {
183         QMessageBox::warning(this, tr("Map file error"), err_str);
184         g_free(err_str);
185         return;
186     }
187     QDesktopServices::openUrl(QUrl::fromLocalFile(gchar_free_to_qstring(map_path)));
188 }
189 #endif
190
191 void EndpointDialog::on_buttonBox_helpRequested()
192 {
193     wsApp->helpTopicAction(HELP_STATS_ENDPOINTS_DIALOG);
194 }
195
196 void init_endpoint_table(struct register_ct* ct, const char *filter)
197 {
198     wsApp->emitStatCommandSignal("Endpoints", filter, GINT_TO_POINTER(get_conversation_proto_id(ct)));
199 }
200
201 // EndpointTreeWidgetItem
202 // TrafficTableTreeWidgetItem / QTreeWidgetItem subclass that allows sorting
203
204 #ifdef HAVE_GEOIP
205 static const char *geoip_none_ = UTF8_EM_DASH;
206 #endif
207
208 class EndpointTreeWidgetItem : public TrafficTableTreeWidgetItem
209 {
210 public:
211     EndpointTreeWidgetItem(GArray *conv_array, guint conv_idx, bool *resolve_names_ptr) :
212         TrafficTableTreeWidgetItem(NULL),
213         conv_array_(conv_array),
214         conv_idx_(conv_idx),
215         resolve_names_ptr_(resolve_names_ptr)
216     {}
217
218     hostlist_talker_t *hostlistTalker() {
219         return &g_array_index(conv_array_, hostlist_talker_t, conv_idx_);
220     }
221
222     virtual QVariant data(int column, int role) const {
223         if (role == Qt::DisplayRole) {
224             // Column text cooked representation.
225             hostlist_talker_t *endp_item = &g_array_index(conv_array_, hostlist_talker_t, conv_idx_);
226
227             bool resolve_names = false;
228             if (resolve_names_ptr_ && *resolve_names_ptr_) resolve_names = true;
229             switch (column) {
230             case ENDP_COLUMN_PACKETS:
231                 return QString("%L1").arg(endp_item->tx_frames + endp_item->rx_frames);
232             case ENDP_COLUMN_BYTES:
233                 return gchar_free_to_qstring(format_size(endp_item->tx_bytes + endp_item->rx_bytes, format_size_unit_none|format_size_prefix_si));
234             case ENDP_COLUMN_PKT_AB:
235                 return QString("%L1").arg(endp_item->tx_frames);
236             case ENDP_COLUMN_BYTES_AB:
237                 return gchar_free_to_qstring(format_size(endp_item->tx_bytes, format_size_unit_none|format_size_prefix_si));
238             case ENDP_COLUMN_PKT_BA:
239                 return QString("%L1").arg(endp_item->rx_frames);
240             case ENDP_COLUMN_BYTES_BA:
241                 return gchar_free_to_qstring(format_size(endp_item->rx_bytes, format_size_unit_none|format_size_prefix_si));
242 #ifdef HAVE_GEOIP
243             default:
244             {
245                 QString geoip_str = colData(column, resolve_names, true).toString();
246                 if (geoip_str.isEmpty()) geoip_str = geoip_none_;
247                 return geoip_str;
248             }
249 #else
250             default:
251                 return colData(column, resolve_names, true);
252 #endif
253             }
254         }
255         return QTreeWidgetItem::data(column, role);
256     }
257
258     // Column text raw representation.
259     // Return a string, qulonglong, double, or invalid QVariant representing the raw column data.
260     QVariant colData(int col, bool resolve_names, bool strings_only) const {
261 #ifndef HAVE_GEOIP
262         Q_UNUSED(strings_only)
263 #endif
264         hostlist_talker_t *endp_item = &g_array_index(conv_array_, hostlist_talker_t, conv_idx_);
265
266         switch (col) {
267         case ENDP_COLUMN_ADDR:
268         {
269             char* addr_str = get_conversation_address(NULL, &endp_item->myaddress, resolve_names);
270             QString q_addr_str(addr_str);
271             wmem_free(NULL, addr_str);
272             return q_addr_str;
273         }
274         case ENDP_COLUMN_PORT:
275             if (resolve_names) {
276                 char* port_str = get_conversation_port(NULL, endp_item->port, endp_item->etype, resolve_names);
277                 QString q_port_str(port_str);
278                 wmem_free(NULL, port_str);
279                 return q_port_str;
280             } else {
281                 return quint32(endp_item->port);
282             }
283         case ENDP_COLUMN_PACKETS:
284             return quint64(endp_item->tx_frames + endp_item->rx_frames);
285         case ENDP_COLUMN_BYTES:
286             return quint64(endp_item->tx_bytes + endp_item->rx_bytes);
287         case ENDP_COLUMN_PKT_AB:
288             return quint64(endp_item->tx_frames);
289         case ENDP_COLUMN_BYTES_AB:
290             return quint64(endp_item->tx_bytes);
291         case ENDP_COLUMN_PKT_BA:
292             return quint64(endp_item->rx_frames);
293         case ENDP_COLUMN_BYTES_BA:
294             return quint64(endp_item->rx_bytes);
295 #ifdef HAVE_GEOIP
296         default:
297         {
298             QString geoip_str;
299             /* Filled in from the GeoIP config, if any */
300             EndpointTreeWidget *ep_tree = qobject_cast<EndpointTreeWidget *>(treeWidget());
301             if (!ep_tree) return geoip_str;
302             foreach (unsigned db, ep_tree->columnToDb(col)) {
303                 if (endp_item->myaddress.type == AT_IPv4) {
304                     geoip_str = geoip_db_lookup_ipv4(db, pntoh32(endp_item->myaddress.data), NULL);
305                 } else if (endp_item->myaddress.type == AT_IPv6) {
306                     const ws_in6_addr *addr = (const ws_in6_addr *) endp_item->myaddress.data;
307                     geoip_str = geoip_db_lookup_ipv6(db, *addr, NULL);
308                 }
309                 if (!geoip_str.isEmpty()) {
310                     break;
311                 }
312             }
313
314             if (strings_only) return geoip_str;
315
316             bool ok;
317
318             double dval = geoip_str.toDouble(&ok);
319             if (ok) { // Assume lat / lon
320                 return dval;
321             }
322
323             qulonglong ullval = geoip_str.toULongLong(&ok);
324             if (ok) { // Assume uint
325                 return ullval;
326             }
327
328             qlonglong llval = geoip_str.toLongLong(&ok);
329             if (ok) { // Assume int
330                 return llval;
331             }
332
333             return geoip_str;
334         }
335 #else
336         default:
337             return QVariant();
338 #endif
339         }
340     }
341
342     virtual QVariant colData(int col, bool resolve_names) const { return colData(col, resolve_names, false); }
343
344     bool operator< (const QTreeWidgetItem &other) const
345     {
346         const EndpointTreeWidgetItem *other_row = static_cast<const EndpointTreeWidgetItem *>(&other);
347         hostlist_talker_t *endp_item = &g_array_index(conv_array_, hostlist_talker_t, conv_idx_);
348         hostlist_talker_t *other_item = &g_array_index(other_row->conv_array_, hostlist_talker_t, other_row->conv_idx_);
349
350         int sort_col = treeWidget()->sortColumn();
351
352         switch(sort_col) {
353         case ENDP_COLUMN_ADDR:
354             return cmp_address(&endp_item->myaddress, &other_item->myaddress) < 0 ? true : false;
355         case ENDP_COLUMN_PORT:
356             return endp_item->port < other_item->port;
357         case ENDP_COLUMN_PACKETS:
358             return (endp_item->tx_frames + endp_item->rx_frames) < (other_item->tx_frames + other_item->rx_frames);
359         case ENDP_COLUMN_BYTES:
360             return (endp_item->tx_bytes + endp_item->rx_bytes) < (other_item->tx_bytes + other_item->rx_bytes);
361         case ENDP_COLUMN_PKT_AB:
362             return endp_item->tx_frames < other_item->tx_frames;
363         case ENDP_COLUMN_BYTES_AB:
364             return endp_item->tx_bytes < other_item->tx_bytes;
365         case ENDP_COLUMN_PKT_BA:
366             return endp_item->rx_frames < other_item->rx_frames;
367         case ENDP_COLUMN_BYTES_BA:
368             return endp_item->rx_bytes < other_item->rx_bytes;
369 #ifdef HAVE_GEOIP
370         default:
371         {
372             double ei_val, oi_val;
373             bool ei_ok, oi_ok;
374             ei_val = text(sort_col).toDouble(&ei_ok);
375             oi_val = other.text(sort_col).toDouble(&oi_ok);
376
377             if (ei_ok && oi_ok) { // Assume lat / lon
378                 return ei_val < oi_val;
379             } else {
380                 // XXX Fall back to string comparison. We might want to try sorting naturally
381                 // using QCollator instead.
382                 return text(sort_col) < other.text(sort_col);
383             }
384         }
385 #else
386         default:
387             return false;
388 #endif
389         }
390     }
391 private:
392     GArray *conv_array_;
393     guint conv_idx_;
394     bool *resolve_names_ptr_;
395 };
396
397 //
398 // EndpointTreeWidget
399 // TrafficTableTreeWidget / QTreeWidget subclass that allows tapping
400 //
401
402 EndpointTreeWidget::EndpointTreeWidget(QWidget *parent, register_ct_t *table) :
403     TrafficTableTreeWidget(parent, table)
404 #ifdef HAVE_GEOIP
405   , has_geoip_data_(false)
406 #endif
407 {
408     setColumnCount(ENDP_NUM_COLUMNS);
409     setUniformRowHeights(true);
410
411     for (int i = 0; i < ENDP_NUM_COLUMNS; i++) {
412         headerItem()->setText(i, endp_column_titles[i]);
413     }
414
415     if (get_conversation_hide_ports(table_)) {
416         hideColumn(ENDP_COLUMN_PORT);
417     } else if (!strcmp(proto_get_protocol_filter_name(get_conversation_proto_id(table_)), "ncp")) {
418         headerItem()->setText(ENDP_COLUMN_PORT, endp_conn_title);
419     }
420
421 #ifdef HAVE_GEOIP
422     QMap<QString, int> db_name_to_col;
423     for (unsigned db = 0; db < geoip_db_num_dbs(); db++) {
424         QString db_name = geoip_db_name(db);
425         int col = db_name_to_col.value(db_name, -1);
426
427         if (col < 0) {
428             col = columnCount();
429             setColumnCount(col + 1);
430             headerItem()->setText(col, db_name);
431             hideColumn(col);
432             db_name_to_col[db_name] = col;
433         }
434         col_to_db_[col] << db;
435     }
436 #endif
437
438     int one_en = fontMetrics().height() / 2;
439     for (int i = 0; i < columnCount(); i++) {
440         switch (i) {
441         case ENDP_COLUMN_ADDR:
442             setColumnWidth(i, one_en * (int) strlen("000.000.000.000"));
443             break;
444         case ENDP_COLUMN_PORT:
445             setColumnWidth(i, one_en * (int) strlen("000000"));
446             break;
447         case ENDP_COLUMN_PACKETS:
448         case ENDP_COLUMN_PKT_AB:
449         case ENDP_COLUMN_PKT_BA:
450             setColumnWidth(i, one_en * (int) strlen("00,000"));
451             break;
452         case ENDP_COLUMN_BYTES:
453         case ENDP_COLUMN_BYTES_AB:
454         case ENDP_COLUMN_BYTES_BA:
455             setColumnWidth(i, one_en * (int) strlen("000,000"));
456             break;
457         default:
458             setColumnWidth(i, one_en * (int) strlen("-00.000000")); // GeoIP
459         }
460     }
461
462     QMenu *submenu;
463
464     FilterAction::Action cur_action = FilterAction::ActionApply;
465     submenu = ctx_menu_.addMenu(FilterAction::actionName(cur_action));
466     foreach (FilterAction::ActionType at, FilterAction::actionTypes()) {
467         FilterAction *fa = new FilterAction(submenu, cur_action, at);
468         submenu->addAction(fa);
469         connect(fa, SIGNAL(triggered()), this, SLOT(filterActionTriggered()));
470     }
471
472     cur_action = FilterAction::ActionPrepare;
473     submenu = ctx_menu_.addMenu(FilterAction::actionName(cur_action));
474     foreach (FilterAction::ActionType at, FilterAction::actionTypes()) {
475         FilterAction *fa = new FilterAction(submenu, cur_action, at);
476         submenu->addAction(fa);
477         connect(fa, SIGNAL(triggered()), this, SLOT(filterActionTriggered()));
478     }
479
480     cur_action = FilterAction::ActionFind;
481     submenu = ctx_menu_.addMenu(FilterAction::actionName(cur_action));
482     foreach (FilterAction::ActionType at, FilterAction::actionTypes()) {
483         FilterAction *fa = new FilterAction(submenu, cur_action, at);
484         submenu->addAction(fa);
485         connect(fa, SIGNAL(triggered()), this, SLOT(filterActionTriggered()));
486     }
487
488     cur_action = FilterAction::ActionColorize;
489     submenu = ctx_menu_.addMenu(FilterAction::actionName(cur_action));
490     foreach (FilterAction::ActionType at, FilterAction::actionTypes()) {
491         FilterAction *fa = new FilterAction(submenu, cur_action, at);
492         submenu->addAction(fa);
493         connect(fa, SIGNAL(triggered()), this, SLOT(filterActionTriggered()));
494     }
495
496     updateItems();
497
498 }
499
500 EndpointTreeWidget::~EndpointTreeWidget()
501 {
502     reset_hostlist_table_data(&hash_);
503 }
504
505 void EndpointTreeWidget::tapReset(void *conv_hash_ptr)
506 {
507     conv_hash_t *hash = (conv_hash_t*)conv_hash_ptr;
508     EndpointTreeWidget *endp_tree = qobject_cast<EndpointTreeWidget *>((EndpointTreeWidget *)hash->user_data);
509     if (!endp_tree) return;
510
511     endp_tree->clear();
512     reset_hostlist_table_data(&endp_tree->hash_);
513 }
514
515 void EndpointTreeWidget::tapDraw(void *conv_hash_ptr)
516 {
517     conv_hash_t *hash = (conv_hash_t*)conv_hash_ptr;
518     EndpointTreeWidget *endp_tree = qobject_cast<EndpointTreeWidget *>((EndpointTreeWidget *)hash->user_data);
519     if (!endp_tree) return;
520
521     endp_tree->updateItems();
522 }
523
524 void EndpointTreeWidget::updateItems()
525 {
526     bool resize = topLevelItemCount() < resizeThreshold();
527     title_ = proto_get_protocol_short_name(find_protocol_by_id(get_conversation_proto_id(table_)));
528
529     if (hash_.conv_array && hash_.conv_array->len > 0) {
530         title_.append(QString(" %1 %2").arg(UTF8_MIDDLE_DOT).arg(hash_.conv_array->len));
531     }
532     emit titleChanged(this, title_);
533
534     if (!hash_.conv_array) {
535         return;
536     }
537
538 #ifdef HAVE_GEOIP
539     if (topLevelItemCount() < 1 && hash_.conv_array->len > 0) {
540         hostlist_talker_t *endp_item = &g_array_index(hash_.conv_array, hostlist_talker_t, 0);
541         if (endp_item->myaddress.type == AT_IPv4 || endp_item->myaddress.type == AT_IPv6) {
542             for (unsigned i = 0; i < geoip_db_num_dbs(); i++) {
543                 showColumn(ENDP_NUM_COLUMNS + i);
544             }
545             has_geoip_data_ = true;
546             emit geoIPStatusChanged();
547         }
548     }
549 #endif
550
551     setSortingEnabled(false);
552
553     QList<QTreeWidgetItem *>new_items;
554     for (int i = topLevelItemCount(); i < (int) hash_.conv_array->len; i++) {
555         EndpointTreeWidgetItem *etwi = new EndpointTreeWidgetItem(hash_.conv_array, i, &resolve_names_);
556         new_items << etwi;
557
558         for (int col = 0; col < columnCount(); col++) {
559             if (col != ENDP_COLUMN_ADDR && col < ENDP_NUM_COLUMNS) {
560                 etwi->setTextAlignment(col, Qt::AlignRight);
561             }
562         }
563     }
564     addTopLevelItems(new_items);
565     setSortingEnabled(true);
566
567     if (resize) {
568         for (int col = 0; col < columnCount(); col++) {
569             resizeColumnToContents(col);
570         }
571     }
572 }
573
574 void EndpointTreeWidget::filterActionTriggered()
575 {
576     EndpointTreeWidgetItem *etwi = static_cast<EndpointTreeWidgetItem *>(currentItem());
577     FilterAction *fa = qobject_cast<FilterAction *>(QObject::sender());
578
579     if (!fa || !etwi) {
580         return;
581     }
582
583     hostlist_talker_t *endp_item = etwi->hostlistTalker();
584     if (!endp_item) {
585         return;
586     }
587
588     QString filter = get_hostlist_filter(endp_item);
589     emit filterAction(filter, fa->action(), fa->actionType());
590 }
591
592 /*
593  * Editor modelines
594  *
595  * Local Variables:
596  * c-basic-offset: 4
597  * tab-width: 8
598  * indent-tabs-mode: nil
599  * End:
600  *
601  * ex: set shiftwidth=4 tabstop=8 expandtab:
602  * :indentSize=4:tabSize=8:noTabs=true:
603  */