1 /**
2 * Copyright (c) 2015, Timothy Stack
3 *
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are met:
8 *
9 * * Redistributions of source code must retain the above copyright notice, this
10 * list of conditions and the following disclaimer.
11 * * Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions and the following disclaimer in the documentation
13 * and/or other materials provided with the distribution.
14 * * Neither the name of Timothy Stack nor the names of its contributors
15 * may be used to endorse or promote products derived from this software
16 * without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
19 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
22 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29
30 #include "config.h"
31
32 #include <assert.h>
33 #include <unistd.h>
34 #include <string.h>
35
36 #include "lnav.hh"
37 #include "base/injector.bind.hh"
38 #include "base/lnav_log.hh"
39 #include "sql_util.hh"
40 #include "views_vtab.hh"
41 #include "view_curses.hh"
42
43 using namespace std;
44
45 template<>
46 struct from_sqlite<lnav_view_t> {
operator ()from_sqlite47 inline lnav_view_t operator()(int argc, sqlite3_value **val, int argi) {
48 const char *view_name = (const char *) sqlite3_value_text(val[argi]);
49 auto view_index_opt = view_from_string(view_name);
50
51 if (!view_index_opt) {
52 throw from_sqlite_conversion_error("lnav view name", argi);
53 }
54
55 return view_index_opt.value();
56 }
57 };
58
59 template<>
60 struct from_sqlite<text_filter::type_t> {
operator ()from_sqlite61 inline text_filter::type_t operator()(int argc, sqlite3_value **val, int argi) {
62 const char *type_name = (const char *) sqlite3_value_text(val[argi]);
63
64 if (strcasecmp(type_name, "in") == 0) {
65 return text_filter::INCLUDE;
66 } else if (strcasecmp(type_name, "out") == 0) {
67 return text_filter::EXCLUDE;
68 }
69
70 throw from_sqlite_conversion_error("filter type", argi);
71 }
72 };
73
74 template<>
75 struct from_sqlite<pair<string, auto_mem<pcre>>> {
operator ()from_sqlite76 inline pair<string, auto_mem<pcre>> operator()(int argc, sqlite3_value **val, int argi) {
77 const char *pattern = (const char *) sqlite3_value_text(val[argi]);
78 const char *errptr;
79 auto_mem<pcre> code;
80 int eoff;
81
82 if (pattern == nullptr || pattern[0] == '\0') {
83 throw from_sqlite_conversion_error("non-empty pattern", argi);
84 }
85
86 code = pcre_compile(pattern,
87 PCRE_CASELESS,
88 &errptr,
89 &eoff,
90 nullptr);
91
92 if (code == nullptr) {
93 throw sqlite_func_error(
94 "Invalid regular expression in column {}: {} at offset {}",
95 argi, errptr, eoff);
96 }
97
98 return make_pair(string(pattern), std::move(code));
99 }
100 };
101
102 struct lnav_views : public tvt_iterator_cursor<lnav_views> {
103 static constexpr const char *NAME = "lnav_views";
104 static constexpr const char *CREATE_STMT = R"(
105 -- Access lnav's views through this table.
106 CREATE TABLE lnav_views (
107 name TEXT PRIMARY KEY, -- The name of the view.
108 top INTEGER, -- The number of the line at the top of the view, starting from zero.
109 left INTEGER, -- The left position of the viewport.
110 height INTEGER, -- The height of the viewport.
111 inner_height INTEGER, -- The number of lines in the view.
112 top_time DATETIME, -- The time of the top line in the view, if the content is time-based.
113 top_file TEXT, -- The file the top line is from.
114 paused INTEGER, -- Indicates if the view is paused and will not load new data.
115 search TEXT, -- The text to search for in the view.
116 filtering INTEGER -- Indicates if the view is applying filters.
117 );
118 )";
119
120 using iterator = textview_curses *;
121
beginlnav_views122 iterator begin() {
123 return std::begin(lnav_data.ld_views);
124 }
125
endlnav_views126 iterator end() {
127 return std::end(lnav_data.ld_views);
128 }
129
get_columnlnav_views130 int get_column(cursor &vc, sqlite3_context *ctx, int col) {
131 lnav_view_t view_index = (lnav_view_t) distance(std::begin(lnav_data.ld_views), vc.iter);
132 textview_curses &tc = *vc.iter;
133 unsigned long width;
134 vis_line_t height;
135
136 tc.get_dimensions(height, width);
137 switch (col) {
138 case 0:
139 sqlite3_result_text(ctx,
140 lnav_view_strings[view_index], -1,
141 SQLITE_STATIC);
142 break;
143 case 1:
144 sqlite3_result_int(ctx, (int) tc.get_top());
145 break;
146 case 2:
147 sqlite3_result_int(ctx, tc.get_left());
148 break;
149 case 3:
150 sqlite3_result_int(ctx, height);
151 break;
152 case 4:
153 sqlite3_result_int(ctx, tc.get_inner_height());
154 break;
155 case 5: {
156 auto *time_source = dynamic_cast<text_time_translator *>(tc.get_sub_source());
157
158 if (time_source != nullptr && tc.get_inner_height() > 0) {
159 auto top_time_opt = time_source->time_for_row(tc.get_top());
160
161 if (top_time_opt) {
162 char timestamp[64];
163
164 sql_strftime(timestamp, sizeof(timestamp), top_time_opt.value(), 'T');
165 sqlite3_result_text(ctx, timestamp, -1, SQLITE_TRANSIENT);
166 } else {
167 sqlite3_result_null(ctx);
168 }
169 } else {
170 sqlite3_result_null(ctx);
171 }
172 break;
173 }
174 case 6: {
175 to_sqlite(ctx, tc.map_top_row([](const auto& al) {
176 return get_string_attr(al.get_attrs(), &logline::L_FILE) | [](const auto* sa) {
177 auto lf = (logfile *) sa->sa_value.sav_ptr;
178
179 return nonstd::make_optional(lf->get_filename());
180 };
181 }));
182 break;
183 }
184 case 7:
185 sqlite3_result_int(ctx, tc.is_paused());
186 break;
187 case 8:
188 to_sqlite(ctx, tc.get_current_search());
189 break;
190 case 9: {
191 auto tss = tc.get_sub_source();
192
193 if (tss != nullptr && tss->tss_supports_filtering) {
194 sqlite3_result_int(ctx, tss->tss_apply_filters);
195 } else {
196 sqlite3_result_int(ctx, 0);
197 }
198 break;
199 }
200 }
201
202 return SQLITE_OK;
203 }
204
delete_rowlnav_views205 int delete_row(sqlite3_vtab *tab, sqlite3_int64 rowid) {
206 tab->zErrMsg = sqlite3_mprintf(
207 "Rows cannot be deleted from the lnav_views table");
208 return SQLITE_ERROR;
209 }
210
insert_rowlnav_views211 int insert_row(sqlite3_vtab *tab, sqlite3_int64 &rowid_out) {
212 tab->zErrMsg = sqlite3_mprintf(
213 "Rows cannot be inserted into the lnav_views table");
214 return SQLITE_ERROR;
215 };
216
update_rowlnav_views217 int update_row(sqlite3_vtab *tab,
218 sqlite3_int64 &index,
219 const char *name,
220 int64_t top_row,
221 int64_t left,
222 int64_t height,
223 int64_t inner_height,
224 const char *top_time,
225 const char *top_file,
226 bool is_paused,
227 const char *search,
228 bool do_filtering) {
229 textview_curses &tc = lnav_data.ld_views[index];
230 text_time_translator *time_source = dynamic_cast<text_time_translator *>(tc.get_sub_source());
231
232 if (tc.get_top() != top_row) {
233 tc.set_top(vis_line_t(top_row));
234 } else if (top_time != nullptr && time_source != nullptr) {
235 date_time_scanner dts;
236 struct timeval tv;
237
238 if (dts.convert_to_timeval(top_time, -1, nullptr, tv)) {
239 auto last_time_opt = time_source->time_for_row(tc.get_top());
240
241 if (last_time_opt) {
242 auto last_time = last_time_opt.value();
243 if (tv != last_time) {
244 time_source->row_for_time(tv) | [&tc](auto row) {
245 tc.set_top(row);
246 };
247 }
248 }
249 } else {
250 tab->zErrMsg = sqlite3_mprintf("Invalid time: %s", top_time);
251 return SQLITE_ERROR;
252 }
253 }
254 tc.set_left(left);
255 tc.set_paused(is_paused);
256 tc.execute_search(search);
257 auto tss = tc.get_sub_source();
258 if (tss != nullptr &&
259 tss->tss_supports_filtering &&
260 tss->tss_apply_filters != do_filtering) {
261 tss->tss_apply_filters = do_filtering;
262 tss->text_filters_changed();
263 }
264
265 return SQLITE_OK;
266 };
267 };
268
269 struct lnav_view_stack : public tvt_iterator_cursor<lnav_view_stack> {
270 using iterator = vector<textview_curses *>::iterator;
271
272 static constexpr const char *NAME = "lnav_view_stack";
273 static constexpr const char *CREATE_STMT = R"(
274 -- Access lnav's view stack through this table.
275 CREATE TABLE lnav_view_stack (
276 name TEXT
277 );
278 )";
279
beginlnav_view_stack280 iterator begin() {
281 return lnav_data.ld_view_stack.begin();
282 }
283
endlnav_view_stack284 iterator end() {
285 return lnav_data.ld_view_stack.end();
286 }
287
get_columnlnav_view_stack288 int get_column(cursor &vc, sqlite3_context *ctx, int col) {
289 textview_curses *tc = *vc.iter;
290 auto view = lnav_view_t(tc - lnav_data.ld_views);
291
292 switch (col) {
293 case 0:
294 sqlite3_result_text(ctx,
295 lnav_view_strings[view], -1,
296 SQLITE_STATIC);
297 break;
298 }
299
300 return SQLITE_OK;
301 };
302
delete_rowlnav_view_stack303 int delete_row(sqlite3_vtab *tab, sqlite3_int64 rowid) {
304 if ((size_t)rowid != lnav_data.ld_view_stack.size() - 1) {
305 tab->zErrMsg = sqlite3_mprintf(
306 "Only the top view in the stack can be deleted");
307 return SQLITE_ERROR;
308 }
309
310 lnav_data.ld_last_view = *lnav_data.ld_view_stack.top();
311 lnav_data.ld_view_stack.pop_back();
312 return SQLITE_OK;
313 };
314
insert_rowlnav_view_stack315 int insert_row(sqlite3_vtab *tab,
316 sqlite3_int64 &rowid_out,
317 lnav_view_t view_index) {
318 textview_curses *tc = &lnav_data.ld_views[view_index];
319
320 ensure_view(tc);
321 rowid_out = lnav_data.ld_view_stack.size() - 1;
322
323 return SQLITE_OK;
324 };
325
update_rowlnav_view_stack326 int update_row(sqlite3_vtab *tab, sqlite3_int64 &index) {
327 tab->zErrMsg = sqlite3_mprintf(
328 "The lnav_view_stack table cannot be updated");
329 return SQLITE_ERROR;
330 };
331 };
332
333 struct lnav_view_filter_base {
334 struct iterator {
335 using difference_type = int;
336 using value_type = text_filter;
337 using pointer = text_filter *;
338 using reference = text_filter &;
339 using iterator_category = forward_iterator_tag;
340
341 lnav_view_t i_view_index;
342 int i_filter_index;
343
iteratorlnav_view_filter_base::iterator344 iterator(lnav_view_t view = LNV_LOG, int filter = -1)
345 : i_view_index(view), i_filter_index(filter) {
346 }
347
operator ++lnav_view_filter_base::iterator348 iterator &operator++() {
349 while (this->i_view_index < LNV__MAX) {
350 textview_curses &tc = lnav_data.ld_views[this->i_view_index];
351 text_sub_source *tss = tc.get_sub_source();
352
353 if (tss == nullptr) {
354 this->i_view_index = lnav_view_t(this->i_view_index + 1);
355 continue;
356 }
357
358 filter_stack &fs = tss->get_filters();
359
360 this->i_filter_index += 1;
361 if (this->i_filter_index >= (ssize_t) fs.size()) {
362 this->i_filter_index = -1;
363 this->i_view_index = lnav_view_t(this->i_view_index + 1);
364 } else {
365 break;
366 }
367 }
368
369 return *this;
370 }
371
operator ==lnav_view_filter_base::iterator372 bool operator==(const iterator &other) const {
373 return this->i_view_index == other.i_view_index &&
374 this->i_filter_index == other.i_filter_index;
375 }
376
operator !=lnav_view_filter_base::iterator377 bool operator!=(const iterator &other) const {
378 return !(*this == other);
379 }
380 };
381
beginlnav_view_filter_base382 iterator begin() {
383 iterator retval = iterator();
384
385 return ++retval;
386 }
387
endlnav_view_filter_base388 iterator end() {
389 return {LNV__MAX, -1};
390 }
391
get_rowidlnav_view_filter_base392 sqlite_int64 get_rowid(iterator iter) {
393 textview_curses &tc = lnav_data.ld_views[iter.i_view_index];
394 text_sub_source *tss = tc.get_sub_source();
395 filter_stack &fs = tss->get_filters();
396 auto &tf = *(fs.begin() + iter.i_filter_index);
397
398 sqlite_int64 retval = iter.i_view_index;
399
400 retval = retval << 32;
401 retval = retval | tf->get_index();
402
403 return retval;
404 }
405 };
406
407 struct lnav_view_filters : public tvt_iterator_cursor<lnav_view_filters>,
408 public lnav_view_filter_base {
409 static constexpr const char *NAME = "lnav_view_filters";
410 static constexpr const char *CREATE_STMT = R"(
411 -- Access lnav's filters through this table.
412 CREATE TABLE lnav_view_filters (
413 view_name TEXT, -- The name of the view.
414 filter_id INTEGER DEFAULT 0, -- The filter identifier.
415 enabled INTEGER DEFAULT 1, -- Indicates if the filter is enabled/disabled.
416 type TEXT DEFAULT 'out', -- The type of filter (i.e. in/out).
417 pattern TEXT -- The filter pattern.
418 );
419 )";
420
get_columnlnav_view_filters421 int get_column(cursor &vc, sqlite3_context *ctx, int col) {
422 textview_curses &tc = lnav_data.ld_views[vc.iter.i_view_index];
423 text_sub_source *tss = tc.get_sub_source();
424 filter_stack &fs = tss->get_filters();
425 auto tf = *(fs.begin() + vc.iter.i_filter_index);
426
427 switch (col) {
428 case 0:
429 sqlite3_result_text(ctx,
430 lnav_view_strings[vc.iter.i_view_index], -1,
431 SQLITE_STATIC);
432 break;
433 case 1:
434 to_sqlite(ctx, tf->get_index());
435 break;
436 case 2:
437 sqlite3_result_int(ctx, tf->is_enabled());
438 break;
439 case 3:
440 switch (tf->get_type()) {
441 case text_filter::INCLUDE:
442 sqlite3_result_text(ctx, "in", 2, SQLITE_STATIC);
443 break;
444 case text_filter::EXCLUDE:
445 sqlite3_result_text(ctx, "out", 3, SQLITE_STATIC);
446 break;
447 default:
448 ensure(0);
449 }
450 break;
451 case 4:
452 sqlite3_result_text(ctx,
453 tf->get_id().c_str(),
454 -1,
455 SQLITE_TRANSIENT);
456 break;
457 }
458
459 return SQLITE_OK;
460 }
461
insert_rowlnav_view_filters462 int insert_row(sqlite3_vtab *tab,
463 sqlite3_int64 &rowid_out,
464 lnav_view_t view_index,
465 nonstd::optional<int64_t> _filter_id,
466 nonstd::optional<bool> enabled,
467 nonstd::optional<text_filter::type_t> type,
468 pair<string, auto_mem<pcre>> pattern) {
469 textview_curses &tc = lnav_data.ld_views[view_index];
470 text_sub_source *tss = tc.get_sub_source();
471 filter_stack &fs = tss->get_filters();
472 auto filter_index = fs.next_index();
473 if (!filter_index) {
474 throw sqlite_func_error("Too many filters");
475 }
476 auto pf = make_shared<pcre_filter>(
477 type.value_or(text_filter::type_t::EXCLUDE),
478 pattern.first,
479 *filter_index,
480 pattern.second.release());
481 fs.add_filter(pf);
482 if (!enabled.value_or(true)) {
483 pf->disable();
484 }
485 tss->text_filters_changed();
486 tc.set_needs_update();
487
488 return SQLITE_OK;
489 }
490
delete_rowlnav_view_filters491 int delete_row(sqlite3_vtab *tab, sqlite3_int64 rowid) {
492 auto view_index = lnav_view_t(rowid >> 32);
493 size_t filter_index = rowid & 0xffffffffLL;
494 textview_curses &tc = lnav_data.ld_views[view_index];
495 text_sub_source *tss = tc.get_sub_source();
496 filter_stack &fs = tss->get_filters();
497
498 for (const auto &iter : fs) {
499 if (iter->get_index() == filter_index) {
500 fs.delete_filter(iter->get_id());
501 tss->text_filters_changed();
502 break;
503 }
504 }
505 tc.set_needs_update();
506
507 return SQLITE_OK;
508 }
509
update_rowlnav_view_filters510 int update_row(sqlite3_vtab *tab,
511 sqlite3_int64 &rowid,
512 lnav_view_t new_view_index,
513 int64_t new_filter_id,
514 bool enabled,
515 text_filter::type_t type,
516 pair<string, auto_mem<pcre>> pattern) {
517 auto view_index = lnav_view_t(rowid >> 32);
518 auto filter_index = rowid & 0xffffffffLL;
519 textview_curses &tc = lnav_data.ld_views[view_index];
520 text_sub_source *tss = tc.get_sub_source();
521 filter_stack &fs = tss->get_filters();
522 auto iter = fs.begin();
523 for (; iter != fs.end(); ++iter) {
524 if ((*iter)->get_index() == (size_t) filter_index) {
525 break;
526 }
527 }
528
529 shared_ptr<text_filter> tf = *iter;
530
531 if (new_view_index != view_index) {
532 tab->zErrMsg = sqlite3_mprintf(
533 "The view for a filter cannot be changed");
534 return SQLITE_ERROR;
535 }
536
537 tf->lf_deleted = true;
538 tss->text_filters_changed();
539
540 auto pf = make_shared<pcre_filter>(type,
541 pattern.first,
542 tf->get_index(),
543 pattern.second.release());
544
545 if (!enabled) {
546 pf->disable();
547 }
548
549 *iter = pf;
550 tss->text_filters_changed();
551 tc.set_needs_update();
552
553 return SQLITE_OK;
554 };
555 };
556
557 struct lnav_view_filter_stats : public tvt_iterator_cursor<lnav_view_filter_stats>,
558 public lnav_view_filter_base {
559 static constexpr const char *NAME = "lnav_view_filter_stats";
560 static constexpr const char *CREATE_STMT = R"(
561 -- Access statistics for filters through this table.
562 CREATE TABLE lnav_view_filter_stats (
563 view_name TEXT, -- The name of the view.
564 filter_id INTEGER, -- The filter identifier.
565 hits INTEGER -- The number of lines that matched this filter.
566 );
567 )";
568
get_columnlnav_view_filter_stats569 int get_column(cursor &vc, sqlite3_context *ctx, int col) {
570 textview_curses &tc = lnav_data.ld_views[vc.iter.i_view_index];
571 text_sub_source *tss = tc.get_sub_source();
572 filter_stack &fs = tss->get_filters();
573 auto tf = *(fs.begin() + vc.iter.i_filter_index);
574
575 switch (col) {
576 case 0:
577 sqlite3_result_text(ctx,
578 lnav_view_strings[vc.iter.i_view_index], -1,
579 SQLITE_STATIC);
580 break;
581 case 1:
582 to_sqlite(ctx, tf->get_index());
583 break;
584 case 2:
585 to_sqlite(ctx, tss->get_filtered_count_for(tf->get_index()));
586 break;
587 }
588
589 return SQLITE_OK;
590 }
591 };
592
593 struct lnav_view_files : public tvt_iterator_cursor<lnav_view_files> {
594 static constexpr const char *NAME = "lnav_view_files";
595 static constexpr const char *CREATE_STMT = R"(
596 --
597 CREATE TABLE lnav_view_files (
598 view_name TEXT, -- The name of the view.
599 filepath TEXT, -- The path to the file.
600 visible INTEGER -- Indicates whether or not the file is shown.
601 );
602 )";
603
604 using iterator = logfile_sub_source::iterator;
605
beginlnav_view_files606 iterator begin() {
607 return lnav_data.ld_log_source.begin();
608 }
609
endlnav_view_files610 iterator end() {
611 return lnav_data.ld_log_source.end();
612 }
613
get_columnlnav_view_files614 int get_column(cursor &vc, sqlite3_context *ctx, int col) {
615 auto& ld = *vc.iter;
616
617 switch (col) {
618 case 0:
619 sqlite3_result_text(ctx,
620 lnav_view_strings[LNV_LOG], -1,
621 SQLITE_STATIC);
622 break;
623 case 1:
624 to_sqlite(ctx, ld->ld_filter_state.lfo_filter_state
625 .tfs_logfile->get_filename());
626 break;
627 case 2:
628 to_sqlite(ctx, ld->ld_visible);
629 break;
630 }
631
632 return SQLITE_OK;
633 }
634
delete_rowlnav_view_files635 int delete_row(sqlite3_vtab *tab, sqlite3_int64 rowid) {
636 tab->zErrMsg = sqlite3_mprintf(
637 "Rows cannot be deleted from the lnav_view_files table");
638 return SQLITE_ERROR;
639 }
640
insert_rowlnav_view_files641 int insert_row(sqlite3_vtab *tab, sqlite3_int64 &rowid_out) {
642 tab->zErrMsg = sqlite3_mprintf(
643 "Rows cannot be inserted into the lnav_view_files table");
644 return SQLITE_ERROR;
645 };
646
update_rowlnav_view_files647 int update_row(sqlite3_vtab *tab,
648 sqlite3_int64 &rowid,
649 const char *view_name,
650 const char *file_path,
651 bool visible) {
652 auto &lss = lnav_data.ld_log_source;
653 auto iter = this->begin();
654
655 std::advance(iter, rowid);
656
657 auto& ld = *iter;
658 if (ld->ld_visible != visible) {
659 ld->set_visibility(visible);
660 lss.text_filters_changed();
661 }
662
663 return SQLITE_OK;
664 }
665 };
666
667 static const char *CREATE_FILTER_VIEW = R"(
668 CREATE VIEW lnav_view_filters_and_stats AS
669 SELECT * FROM lnav_view_filters LEFT NATURAL JOIN lnav_view_filter_stats
670 )";
671
672 static auto a = injector::bind_multiple<vtab_module_base>()
673 .add<vtab_module<lnav_views>>()
674 .add<vtab_module<lnav_view_stack>>()
675 .add<vtab_module<lnav_view_filters>>()
676 .add<vtab_module<tvt_no_update<lnav_view_filter_stats>>>()
677 .add<vtab_module<lnav_view_files>>();
678
register_views_vtab(sqlite3 * db)679 int register_views_vtab(sqlite3 *db)
680 {
681 char *errmsg;
682 if (sqlite3_exec(db, CREATE_FILTER_VIEW, nullptr, nullptr, &errmsg) != SQLITE_OK) {
683 log_error("Unable to create filter view: %s", errmsg);
684 }
685
686 return 0;
687 }
688